{"openapi":"3.1.0","info":{"title":"teryt24.pl API","version":"0.1.0","description":"Polish registry & geo-data API for the *24-pl project family.\n\nData sources:\n- **GUS TERYT** (SIMC/TERC/ULIC) — cities, localities, administrative hierarchy, streets\n- **GeoNames** postal export (CC BY 4.0) — postal codes\n- **GUGiK PRG** — address points (opt-in) for /v1/geocode and /v1/reverse-geocode\n\n## Identification and rate limits\n\nKeyless calls are rate-limited to **60 requests/min/IP** — the same budget is shared by every anonymous caller behind one address.\n\nIdentify yourself to get out of the shared bucket: present an API key in the `X-API-Key` header (`t24_…`, issued by the operator — each key carries its own limit and its own bucket) and, in the spirit of Nominatim's usage policy, set a meaningful `User-Agent` naming your application; stock library defaults (`axios/1`, `Go-http-client/1.1`) describe your HTTP stack, not you. Browser-based callers are attributed by their `Origin`/`Referer` host automatically.\n\n`GET /v1/whoami` echoes what this service records about your requests; the acceptable-use policy (limits, identification, attribution, prohibited uses) lives at `/usage-policy`.\n\nAll /v1 endpoints are rate-limited. Successful GET responses carry an `ETag` and `Cache-Control: public, max-age=86400`."},"servers":[{"url":"/"}],"tags":[{"name":"health"},{"name":"cities"},{"name":"localities"},{"name":"regions"},{"name":"streets"},{"name":"postal-codes"},{"name":"geocoding"},{"name":"meta"}],"paths":{"/health":{"get":{"tags":["health"],"summary":"Liveness/readiness (used by the Docker healthcheck)","responses":{"200":{"description":"Healthy (or DB-less) — see `data` for dataset freshness; `stale` is advisory only.","content":{"application/json":{"schema":{"type":"object"}}}},"503":{"description":"Degraded — database unreachable.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/cities":{"get":{"tags":["cities"],"summary":"Polish city autocomplete (GUS SIMC, ≈1,050 entries)","description":"Behavioral port of pacjenteo's GET /api/v1/cities — drop-in compatible.","parameters":[{"name":"q","in":"query","required":true,"description":"Search query; diacritics- and punctuation-insensitive (Lodz finds Łódź).","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"limit","in":"query","required":false,"description":"Maximum results per page (1–50).","schema":{"default":10,"type":"integer","minimum":1,"maximum":50}},{"name":"mode","in":"query","required":false,"description":"autocomplete = prefix-first, resolve = exact only, hybrid = combined.","schema":{"default":"autocomplete","type":"string","enum":["autocomplete","resolve","hybrid"]}},{"name":"rm","in":"query","required":false,"description":"Optional 2-digit rodzaj-miejscowości filter (96 = miasto, 95 = dzielnica Warszawy, 98 = delegatura).","schema":{"type":"string","pattern":"^\\d{2}$"}},{"name":"province","in":"query","required":false,"description":"Optional 2-digit TERC voivodeship code filter (e.g. '14'), as served by /v1/regions.","schema":{"type":"string","pattern":"^\\d{2}$"}},{"name":"county","in":"query","required":false,"description":"Optional 4-digit TERC code filter (WOJ+POW, e.g. '1465').","schema":{"type":"string","pattern":"^\\d{4}$"}},{"name":"municipality","in":"query","required":false,"description":"Optional 6-digit TERC code filter (WOJ+POW+GMI, e.g. '146503').","schema":{"type":"string","pattern":"^\\d{6}$"}},{"name":"minPopulation","in":"query","required":false,"description":"Optional inhabitants lower bound (GeoNames data; unknown population is excluded).","schema":{"type":"integer","minimum":0,"maximum":50000000}},{"name":"maxPopulation","in":"query","required":false,"description":"Optional inhabitants upper bound (GeoNames data; unknown population is excluded).","schema":{"type":"integer","minimum":0,"maximum":50000000}},{"name":"offset","in":"query","required":false,"description":"Skip the first N results (0-based, max 10000) to page past the limit.","schema":{"default":0,"type":"integer","minimum":0,"maximum":10000}},{"name":"aliases","in":"query","required":false,"description":"Opt-in exonym fallback (aliases=true): when nothing matched by name, consult the GeoNames-derived alias table ('Danzig' finds Gdańsk).","schema":{"default":"false","type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"cities":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"province":{"type":["string","null"]},"county":{"type":["string","null"]},"municipality":{"type":["string","null"]},"id":{"type":["string","null"]},"type":{"type":["string","null"]},"normalized":{"type":"string"},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"coordSource":{"type":["string","null"]},"population":{"type":["number","null"]},"provinceCode":{"type":["string","null"]},"countyCode":{"type":["string","null"]},"municipalityCode":{"type":["string","null"]}},"required":["name","province","county","municipality","id","type","normalized","lat","lon","coordSource","population","provinceCode","countyCode","municipalityCode"]}}},"required":["cities"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Service is warming up.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/localities":{"get":{"tags":["localities"],"summary":"Full locality autocomplete (all self-standing SIMC entries, ≈54k)","description":"Villages, kolonie, osady… No dedupe — same-named localities are all returned; disambiguate via province/county. Geographic mode (lat+lon): nearest-first with distanceKm; needs coordinates (GeoNames enrichment) — results are empty when the dataset is absent. Browse mode (province/county/municipality without q): all matching localities in name order.","parameters":[{"name":"q","in":"query","required":false,"description":"Search query; diacritics- and punctuation-insensitive. Optional in geographic mode (lat+lon given) or when a filter (province/county/municipality/population) is given, required otherwise.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"limit","in":"query","required":false,"description":"Maximum results per page (1–50).","schema":{"default":10,"type":"integer","minimum":1,"maximum":50}},{"name":"mode","in":"query","required":false,"description":"autocomplete = prefix-first, resolve = exact only, hybrid = combined. Ignored in geographic mode.","schema":{"default":"autocomplete","type":"string","enum":["autocomplete","resolve","hybrid"]}},{"name":"rm","in":"query","required":false,"description":"Optional 2-digit rodzaj-miejscowości filter (01 = wieś, 96 = miasto).","schema":{"type":"string","pattern":"^\\d{2}$"}},{"name":"province","in":"query","required":false,"description":"Optional 2-digit TERC voivodeship code filter (e.g. '14'), as served by /v1/regions. Makes q optional (browse mode: all matching localities in name order).","schema":{"type":"string","pattern":"^\\d{2}$"}},{"name":"county","in":"query","required":false,"description":"Optional 4-digit TERC code filter (WOJ+POW, e.g. '1465'). Makes q optional (browse mode).","schema":{"type":"string","pattern":"^\\d{4}$"}},{"name":"municipality","in":"query","required":false,"description":"Optional 6-digit TERC code filter (WOJ+POW+GMI, e.g. '146503'). Makes q optional (browse mode).","schema":{"type":"string","pattern":"^\\d{6}$"}},{"name":"minPopulation","in":"query","required":false,"description":"Optional inhabitants lower bound (GeoNames data; unknown population is excluded). Makes q optional (browse mode).","schema":{"type":"integer","minimum":0,"maximum":50000000}},{"name":"maxPopulation","in":"query","required":false,"description":"Optional inhabitants upper bound (GeoNames data; unknown population is excluded). Makes q optional (browse mode).","schema":{"type":"integer","minimum":0,"maximum":50000000}},{"name":"offset","in":"query","required":false,"description":"Skip the first N results (0-based, max 10000) to page past the limit.","schema":{"default":0,"type":"integer","minimum":0,"maximum":10000}},{"name":"aliases","in":"query","required":false,"description":"Opt-in exonym fallback (aliases=true): when nothing matched by name, consult the GeoNames-derived alias table ('Danzig' finds Gdańsk).","schema":{"default":"false","type":"string","enum":["true","false"]}},{"name":"lat","in":"query","required":false,"description":"WGS84 latitude (-90..90). Together with lon triggers geographic mode: localities within radiusKm, ordered nearest-first (each with distanceKm).","schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lon","in":"query","required":false,"description":"WGS84 longitude (-180..180). Must be given together with lat.","schema":{"type":"number","minimum":-180,"maximum":180}},{"name":"radiusKm","in":"query","required":false,"description":"Geographic-mode radius in kilometres (default 10, max 100).","schema":{"default":10,"type":"number","minimum":0.1,"maximum":100}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"localities":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"province":{"type":["string","null"]},"county":{"type":["string","null"]},"municipality":{"type":["string","null"]},"id":{"type":["string","null"]},"type":{"type":["string","null"]},"typeLabel":{"type":["string","null"]},"normalized":{"type":"string"},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"coordSource":{"type":["string","null"]},"population":{"type":["number","null"]},"provinceCode":{"type":["string","null"]},"countyCode":{"type":["string","null"]},"municipalityCode":{"type":["string","null"]},"distanceKm":{"type":"number"}},"required":["name","province","county","municipality","id","type","typeLabel","normalized","lat","lon","coordSource","population","provinceCode","countyCode","municipalityCode"]}}},"required":["localities"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Service is warming up.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/localities/{sym}":{"get":{"tags":["localities"],"summary":"Locality detail by SYM (identity, hierarchy, postal codes, street count)","description":"One locality's full record: identity and administrative hierarchy from the search index, plus the postal codes serving it (matched gmina → powiat → name, each hit names its scope; postalCodesTruncated: true when the row cap was hit) and the number of registered streets. postalCodes/streetCount are null when the underlying dataset is not available on this instance. Cities resolve here too (a city is an RM 96 locality).","parameters":[{"name":"sym","in":"path","required":true,"description":"Locality SYM (7 digits, as returned by /v1/cities and /v1/localities).","schema":{"type":"string","pattern":"^\\d{7}$"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"locality":{"type":"object","properties":{"name":{"type":"string"},"province":{"type":["string","null"]},"county":{"type":["string","null"]},"municipality":{"type":["string","null"]},"id":{"type":["string","null"]},"type":{"type":["string","null"]},"typeLabel":{"type":["string","null"]},"normalized":{"type":"string"},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"coordSource":{"type":["string","null"]},"population":{"type":["number","null"]},"provinceCode":{"type":["string","null"]},"countyCode":{"type":["string","null"]},"municipalityCode":{"type":["string","null"]}},"required":["name","province","county","municipality","id","type","typeLabel","normalized","lat","lon","coordSource","population","provinceCode","countyCode","municipalityCode"]},"postalCodes":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"city":{"type":"string"},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]},"scope":{"type":"string","enum":["gmina","powiat","name"]}},"required":["code","city","lat","lon","scope"]}},{"type":"null"}]},"postalCodesTruncated":{"type":"boolean"},"streetCount":{"type":["number","null"]}},"required":["locality","postalCodes","streetCount"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"404":{"description":"Unknown locality SYM.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Service is warming up.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/regions":{"get":{"tags":["regions"],"summary":"Voivodeships (TERC)","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"regions":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"kind":{"type":["string","null"]},"nuts":{"type":"array","items":{"type":"string"}}},"required":["code","name","kind","nuts"]}}},"required":["regions"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Service is warming up.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/regions/{code}/counties":{"get":{"tags":["regions"],"summary":"Counties of one voivodeship (incl. miasta na prawach powiatu)","parameters":[{"name":"code","in":"path","required":true,"description":"TERC code: 2 digits for a voivodeship (e.g. '14').","schema":{"type":"string","pattern":"^\\d{2}$"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"counties":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"kind":{"type":["string","null"]},"nuts":{"type":"array","items":{"type":"string"}}},"required":["code","name","kind","nuts"]}}},"required":["counties"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"404":{"description":"Unknown voivodeship code.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Service is warming up.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/regions/{code}/municipalities":{"get":{"tags":["regions"],"summary":"Municipalities (gminy) of a voivodeship, optionally narrowed to one county","parameters":[{"name":"code","in":"path","required":true,"description":"TERC code: 2 digits for a voivodeship (e.g. '14').","schema":{"type":"string","pattern":"^\\d{2}$"}},{"name":"county","in":"query","required":false,"description":"Optional 2-digit TERC powiat code to narrow to one county.","schema":{"type":"string","pattern":"^\\d{2}$"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"municipalities":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"kind":{"type":["string","null"]},"nuts":{"type":"array","items":{"type":"string"}}},"required":["code","name","kind","nuts"]}}},"required":["municipalities"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"404":{"description":"Unknown voivodeship or county code.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Service is warming up.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/streets":{"get":{"tags":["streets"],"summary":"Street autocomplete (GUS ULIC, ≈310k streets) — per locality or country-wide","description":"With city: street search within one locality (pick a locality via /v1/cities or /v1/localities first, then type a street). With q alone: country-wide prefix search — which localities have this street? Rows then carry localityName and the inflated province/county to disambiguate repeated names.","parameters":[{"name":"city","in":"query","required":false,"description":"Locality SYM (7 digits, as returned by /v1/cities and /v1/localities). Optional: omit it (with q) for a country-wide street search.","schema":{"type":"string","pattern":"^\\d{7}$"}},{"name":"q","in":"query","required":false,"description":"Diacritics-insensitive prefix against the street name without its 'ul.' prefix. Required (and triggers country-wide search) when city is absent; optional within a city (omitted → first `limit` streets alphabetically).","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"limit","in":"query","required":false,"description":"Maximum results per page (1–50).","schema":{"default":10,"type":"integer","minimum":1,"maximum":50}},{"name":"offset","in":"query","required":false,"description":"Skip the first N results (0-based, max 10000) to page past the limit.","schema":{"default":0,"type":"integer","minimum":0,"maximum":10000}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"streets":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"cecha":{"type":["string","null"]},"id":{"type":"string"},"city":{"type":"string"},"localityName":{"type":"string"},"province":{"type":["string","null"]},"county":{"type":["string","null"]}},"required":["name","cecha","id","city"]}}},"required":["streets"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"404":{"description":"Unknown locality SYM.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Street dataset not available (fetch it via POST /admin/fetch-data?datasets=ulic).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/postal-codes":{"get":{"tags":["postal-codes"],"summary":"Polish postal-code lookup (PNA → locality, GeoNames-derived)","parameters":[{"name":"q","in":"query","required":true,"description":"Postal code in any form: '00-950', '00950', or a prefix of either ('00-9', '009'); non-digits are ignored.","schema":{"type":"string","minLength":1,"maxLength":10,"pattern":"^\\d[\\d\\s-]*$"}},{"name":"limit","in":"query","required":false,"description":"Maximum results (1–50).","schema":{"default":10,"type":"integer","minimum":1,"maximum":50}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","properties":{"postalCodes":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"city":{"type":"string"},"province":{"type":["string","null"]},"lat":{"type":["number","null"]},"lon":{"type":["number","null"]}},"required":["code","city","province","lat","lon"]}}},"required":["postalCodes"]}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Postal-code dataset not available (generate data/pna CSV via pnpm fetch:pna).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/geocode":{"get":{"tags":["geocoding"],"summary":"Address validation (GUGiK PRG address points, ~8.6M rows)","description":"House-number-level validation: street + number + locality → exact building point. When the number is unknown but the street exists, nearby numbers are suggested. Requires the PRG dataset on the instance (POST /admin/ingest-prg); otherwise 503.","parameters":[{"name":"sym","in":"query","required":true,"description":"Locality SYM (7 digits, from /v1/cities or /v1/localities).","schema":{"type":"string","pattern":"^\\d{7}$"}},{"name":"number","in":"query","required":true,"description":"House number ('21A').","schema":{"type":"string"}},{"name":"street","in":"query","required":false,"description":"Street SYM_UL (5 digits, from /v1/streets). Omit for rural 'locality + number' addresses.","schema":{"type":"string","pattern":"^\\d{5}$"}},{"name":"streetName","in":"query","required":false,"description":"Street name prefix (diacritics-insensitive; 'rynek' finds 'ul. Rynek'), resolved via the ULIC registry. Mutually exclusive with street; several matches → result 'street-ambiguous' with candidates.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum matches (1–50).","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"Resolution result — 'exact' (building points), 'street' (number unknown, suggestions provided), 'street-ambiguous' (streetName matched several streets) or 'none'.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Address dataset not available (POST /admin/ingest-prg).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/reverse-geocode":{"get":{"tags":["geocoding"],"summary":"Reverse geocoding (nearest PRG address point)","description":"Nearest address for a WGS84 coordinate (single exact KNN query over a GiST earthdistance index — great-circle ordering in the database); falls back to the nearest localities (Phase 1 coordinates) when no address point is within 128 km.","parameters":[{"name":"lat","in":"query","required":true,"description":"WGS84 latitude.","schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lon","in":"query","required":true,"description":"WGS84 longitude.","schema":{"type":"number","minimum":-180,"maximum":180}},{"name":"limit","in":"query","required":false,"description":"Maximum matches (1–50).","schema":{"type":"integer","minimum":1,"maximum":50,"default":5}}],"responses":{"200":{"description":"'address' (PRG points, with street/locality/postal) or 'locality' (Phase 1 fallback) or 'none'.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}}}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}},"503":{"description":"Address dataset not available (POST /admin/ingest-prg).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/v1/whoami":{"get":{"tags":["meta"],"summary":"What this service sees about your requests","description":"Echoes the attribution fields recorded for your requests: caller classification, normalized User-Agent family, referrer/origin hostname, Cloudflare country, the API client a presented key resolves to, the rate limit that applies to you, and a link to the usage policy. Intended for integrators verifying their identification: a meaningful User-Agent and/or an `X-API-Key` header (operator-issued, `t24_…`) is what turns an anonymous caller into a named one with a rate-limit bucket of its own. Not cached — the answer is per-caller.","responses":{"200":{"description":"What the service records about this request.","content":{"application/json":{"schema":{"type":"object","properties":{"apiClient":{"type":["string","null"],"description":"Label of the API key presented, null when keyless."},"identified":{"type":"boolean"},"clientClass":{"type":"string","description":"browser | bot | server | unknown."},"userAgent":{"type":"string","description":"Normalized User-Agent family, e.g. 'Chrome/141'."},"refererHost":{"type":["string","null"]},"originHost":{"type":["string","null"]},"country":{"type":["string","null"],"description":"2-letter CF-IPCountry; null when the request bypassed the edge."},"policyUrl":{"type":"string","description":"Path of the usage-policy page (limits, identification, attribution, prohibited uses)."},"rateLimit":{"type":"object","properties":{"max":{"type":"integer"},"windowMs":{"type":"integer"},"scope":{"type":"string"}},"required":["max","windowMs","scope"]}},"required":["apiClient","identified","clientClass","userAgent","refererHost","originHost","country","policyUrl","rateLimit"]}}}},"401":{"description":"The presented X-API-Key is invalid or revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded (60 requests/min/IP). Includes `Retry-After` and `X-RateLimit-*` headers.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"number"},"error":{"type":"string"},"message":{"type":"string"}},"required":["statusCode","error","message"]}}}}}}}}}