Skip to content

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, melaskoli and Laugardalsl all find what they start; Bónus Laugavegi finds the Bónus on Laugavegur.
  • near ranks answers nearer the point first — a bias, never a filter. Send the centre of the map the visitor is looking at.
  • osmTags keeps only those OpenStreetMap tags: amenity:school, shop (any shop), leisure:sports_centre; several are OR-ed, and !highway excludes. Every candidate carries its category and placeType, 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 — and detected is always query: 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