{"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\nAll /v1 endpoints are rate-limited (60 requests/min/IP). 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"}],"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 (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"]}}],"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"]}},"required":["name","province","county","municipality","id","type","normalized","lat","lon","coordSource","population"]}}},"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.","parameters":[{"name":"q","in":"query","required":false,"description":"Search query; diacritics- and punctuation-insensitive. Optional in geographic mode (lat+lon given), required otherwise.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"limit","in":"query","required":false,"description":"Maximum results (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":"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"]},"distanceKm":{"type":"number"}},"required":["name","province","county","municipality","id","type","typeLabel","normalized","lat","lon","coordSource","population"]}}},"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/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 within one locality (GUS ULIC, ≈310k streets)","description":"Pick a locality via /v1/cities or /v1/localities first, then type a street.","parameters":[{"name":"city","in":"query","required":true,"description":"Locality SYM (7 digits, as returned by /v1/cities and /v1/localities).","schema":{"type":"string","pattern":"^\\d{7}$"}},{"name":"q","in":"query","required":false,"description":"Optional; diacritics-insensitive prefix against the street name without its 'ul.' prefix. Omitted → first `limit` streets alphabetically.","schema":{"type":"string","minLength":1,"maxLength":100}},{"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":{"streets":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"cecha":{"type":["string","null"]},"id":{"type":"string"},"city":{"type":"string"}},"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"]}}}}}}}}}