Resolve a place, and read its day
Two endpoints for the question "what is going on here, on this day". POST /v1/resolve
turns anything that identifies a place into candidates you can put on a map.
POST /v1/context/day reads every service that a place and a date can satisfy, in one call.
Neither endpoint names a country and neither names a source. Coverage is declared in the answer, per reading and per section, so a thin result says why instead of looking empty.
Resolve anything that identifies a place
POST https://quicklookup.com/api/v1/resolve
Authorization: Bearer <your API key>
{ "input": "Harpa", "countryCodes": ["IS"], "limit": 5 }
You do not say what the input is. The service detects it, reports the reading it used as
detected, and you can override that with reading when you know better.
| Input looks like | detected |
Resolved by | Worldwide |
|---|---|---|---|
8.8.8.8, 2001:db8::1 |
ip |
IP2Location | ✅ city precision |
way/456, relation/789, N123 |
osm_id |
Overpass | ⛔ hosted extract |
64.15, -21.94, 64°09'N 21°56'W |
coord_string |
local parser | ✅ |
a@b.is |
email |
WHOIS / ccTLD | ✅ country precision |
https://…, example.is/path |
url |
WHOIS / ccTLD | ✅ country precision |
ruv.is |
domain |
WHOIS / ccTLD | ✅ country precision |
+354 555 1234 |
phone |
dialling code | ✅ country precision |
ge2kug |
geohash |
decoded locally | ✅ |
IS, ISL |
country_code |
ISO index | ✅ |
| anything else | query |
the gazetteer, then the country index | ⛔ then ✅ |
Every candidate carries precision — one of country, region, municipality,
locality, street, poi, address, point. Use it to label the pin. A phone number
and a street address both return coordinates; only precision tells you that one of them
means a country.
{
"input": "Harpa", "detected": "query",
"candidates": [{
"label": "Harpa, 2, Austurbakki, Miðborg, Reykjavíkurborg, …",
"lat": 64.1504, "lon": -21.9325,
"precision": "poi",
"bbox": [-21.9340, 64.1490, -21.9310, 64.1510], // [minLon, minLat, maxLon, maxLat]
"osm": { "type": "way", "id": "108335182" },
"category": "amenity", "placeType": "theatre", // OpenStreetMap class and type
"placeGid": "d247943e-b4bc-50a4-9443-c01a121b817f", // gid.is/osm/way/108335182
"countryCode": "IS", "municipality": "Reykjavíkurborg",
"region": "Höfuðborgarsvæðið", "timezone": "Atlantic/Reykjavik",
"confidence": 0.61,
"resolvedVia": ["nominatim.geocode"], "source": "fdw",
"outOfScope": false
}],
"coverage": { "scope": "gazetteer", "detail": "…", "gazetteerCountries": ["IS"] }
}
Three behaviours worth designing against
outOfScope instead of an empty list. A result outside countryCodes is returned and
flagged, not dropped. That is what lets an interface say "that is in Denmark — sign up for
more" rather than "not found", which is indistinguishable from a typo.
coverage explains an empty answer. scope: "global" means the reading reaches
everywhere. scope: "gazetteer" means it is limited to gazetteerCountries, which is
whichever countries the OpenStreetMap services we host currently carry. Free text that
matches no street still resolves if it names a country, in any of a few dozen languages.
geometry is opt-in. Pass "geometry": true to get each candidate's outline. It is off
by default because a municipality's polygon is orders of magnitude larger than the rest of
the answer, and most callers want a point.
Search as you type
A search box asks with PART of a name, and a whole-name search finds nothing for "Melaskól".
Send "mode": "prefix" on every keystroke instead:
POST https://quicklookup.com/api/v1/resolve
{ "input": "Melaskól", "mode": "prefix", "near": { "lat": 64.1466, "lon": -21.9426 },
"osmTags": ["amenity:school"], "limit": 5 }
- Prefix, accents and case.
Melaskól,melaskoliandLaugardalslall find what they start;Bónus Laugavegifinds the Bónus on Laugavegur. nearranks answers nearer the point first — a bias, never a filter. Send the centre of the map the visitor is looking at.osmTagskeeps only those OpenStreetMap tags:amenity:school,shop(any shop),leisure:sports_centre; several are OR-ed, and!highwayexcludes. Every candidate carries itscategoryandplaceType, so a list can show what kind of place each one is.- Order is the source's own — how well it matches weighed against the distance from
near— anddetectedis alwaysquery: in this mode "Mel" is part of a name, not a country code. - Speed is tens of milliseconds, cheap enough for every keystroke; debounce by ~100 ms and drop answers to keystrokes that have been overtaken.
Two limits are the language's, not the index's. An inflection that changes a vowel is not
matched — "Egilsstöðum" (the dative) does not find the town Egilsstaðir, though "Egilsst"
does. And a kind of place written into a name is not a word of its own: "skóli" does not
find Melaskóli, which is what osmTags: ["amenity:school"] is for. geometry is not
available in this mode, and near/osmTags apply only to it.
Read a place's day
POST https://quicklookup.com/api/v1/context/day
{ "location": { "lat": 64.1504, "lon": -21.9325 },
"date": "2026-10-03",
"timezone": "Atlantic/Reykjavik",
"radiusKm": 5 }
location takes coordinates, or { "resolve": "<anything /v1/resolve accepts>" }.
The sections are discovered, not listed. Every published service you can see is offered
the day and the place; those whose contract can be satisfied from those two facts run, and
those that cannot say which input they were missing. A source published tomorrow appears
here by itself. Narrow the read with include: ["<section key>"].
Each section answers three questions a collage is misleading without.
status — why is this section empty?
| Status | Means |
|---|---|
ok |
It answered, with rows |
empty |
It answered; there is nothing for this place and day |
unavailable |
It did not answer — down, throttled, or past the deadline. error says which |
out_of_range |
The day is outside what the source covers |
not_applicable |
It needs more than a place and a day; missingInputs names what |
pending |
Only with Prefer: respond-async: not read yet. pending says why and when to ask again |
empty and unavailable must never render the same. Nothing is happening and we could
not look are different statements about the world.
influence — what did "at this location" mean?
"influence": { "type": "station", "note": "The archive is a set of stations; the nearest
reporting cell answers, and it may be some way off." }
point, radius (with radiusMeters), station, region or country. It is decided by
QuickLookup and echoed back, so a map draws exactly the reach that was used. A client cannot
know that a cruise call draws visitors 25 km inland, that a hazard warning covers a forecast
region rather than a point, or that a source with no location input is answering for the
whole country — so it is not asked to guess.
A section is only shown when it is about the place you asked for
The map's extent and the data's extent are different things. The resolver is worldwide at
country precision; most of the sources hold one country's data. A source that cannot be shown
to cover the place comes back not_applicable with the reason — never ok with another
country's data, which is worse than no answer:
{ "key": "vegagerdin-traffic-counts-daily", "status": "not_applicable",
"note": "This source holds data for IS and cannot be shown to cover JP." }
Two things are checked, and neither is a hand-kept list: a source whose country input falls
back to a default is answering about that country, and a source that takes neither a
location nor a country can only be about the countries we hold data for
(QUICKLOOKUP_DATA_COUNTRIES). Sources that compute from your coordinates — sun and moon,
seasons, weekends — answer anywhere, and so does anything that accepts the country you asked
about, like holidays.
availability — and the note that stops it being misread
A declared window is a property of the relation, not of your coordinate. A weather
archive can declare years of coverage and still hold nothing at a given point, because it is
a set of stations and the nearest one may never have reported. When a section is empty
inside its own declared window, note says so. Do not render that as an absence of weather.
Timing
A day is local. Combining a date with UTC at a place that is not on UTC reads an
interval shifted by the offset — picking up a neighbouring day at one end and dropping the
day's own at the other. The window is built in the place's own zone; on a clocks-forward day
it is correctly 23 hours long. from, to and timezone come back so you can show what
was read.
Sections run in parallel under one budget for the whole call, not one per section. A
section that passes the deadline is reported unavailable and the response is not held for
it, so the slowest source costs its own section rather than the day.
Show the day at once, and fill it in
A day nobody has asked about yet needs its sources called, and some take seconds. Send
Prefer: respond-async and the day is answered within a second: every section that is
stored or computed is there, and the rest come back pending, with the whole answer as
202 Accepted:
{ "sectionsPending": 3,
"pending": { "reason": "sourcing", "retryAfterMs": 1400, "message": "…" },
"sections": [
{ "key": "ctx-weather-metrics", "status": "pending",
"pending": { "reason": "sourcing", "retryAfterMs": 1400,
"message": "One moment, this data is being sourced." } },
{ "key": "ctx-celestial", "status": "ok", "items": [ … ] } ] }
Render what is there, then ask again after retryAfterMs for just the pending keys
(include: ["ctx-weather-metrics", …]); the sections that were still being read are kept
for that ask, so it is answered from memory. Repeat while any is pending. See
Answer now, fill in later.
One section that is not a service
events-near is the fan-out across every visible event feed at once, which is how a person
reads "what is on near here". Occurrences carry distanceMeters and matchedSite. If a
feed could not be read, note names it and its reason — the rest of the answer is still
served.
Ranking, and what confidence means
Candidates come back best first, ordered by confidence, which measures how well a
candidate answers what was typed — not how prominent the place is:
| Score | Meaning |
|---|---|
| 1.00 | the name is exactly what was typed |
| 0.90 | the name starts with what was typed — what typeahead is doing |
| 0.75 | a whole word of the name is what was typed |
| 0.55 | the name contains it |
| 0.30 | no textual match; the source returned it for another reason |
Prominence adds at most 0.05 on top, so it orders ties without ever crossing a band. Accents
and case are folded, so island scores the same as Ísland.
name is the short name on its own and houseNumber is its own field, so nothing needs to
split label on commas.
Repeats are cheap, briefly
Both endpoints sit behind a short, bounded, per-replica cache (60 s by default). It exists because a lookup box asks the same question on every keystroke and re-render, and each one reaches CTX where a foreign-data query runs Python inside PostgreSQL. It is not a freshness mechanism — the mirror underneath decides that — and a day that could not be read completely is never cached, so an outage is never held in front of the next caller.
Where to go next
- Contracts, paging, and errors for the shared error shape.
- Fetch events as an agent for the event feeds behind
events-near. - API reference and test console for the full schemas.