Documentation navigation
Guides
Getting started Boundary searchAPI reference
Overview Health GET/v1/status Countries GET/v1/countries GET/v1/countries/{code} GET/v1/countries/{code}/regions GET/v1/countries/{code}/neighbors Regions GET/v1/regions GET/v1/regions/{id} GET/v1/regions/{id}/cities GET/v1/regions/{id}/children Cities GET/v1/cities GET/v1/cities/{id} GET/v1/cities/{id}/hierarchy GET/v1/cities/nearby Postal Codes GET/v1/postal-codes GET/v1/postal-codes/nearest Timezones GET/v1/timezones GET/v1/timezones/{tzId} IP Geolocation GET/v1/ip/{address} GET/v1/ip/me Search GET/v1/autocomplete GET/v1/reverse GET/v1/resolve GET/v1/search Boundaries GET/v1/boundaries/{geoname_id} Batch POST/v1/batch/cities POST/v1/batch/countries POST/v1/batch/regionsSearch API
Cross-type fuzzy text search
Authentication: X-API-Key header on every request. Get a key.
Returns autocomplete suggestions matching a query string across cities, regions, and countries. Results are ranked by relevance and population. Minimum 2 characters required.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | required | Search query (minimum 2 characters) |
| lang | string | optional | ISO 639-1 language code for localized names (e.g., de, fr, ja) |
| limit | integer | optional | Maximum results to return (1-25, default 10) |
| fields | string | optional | Comma-separated list of fields to include in the response |
Code samples
Response
{
"data": [
{
"id": 5391959,
"name": "San Francisco",
"type": "city",
"country_code": "US",
"population": 873965
}
],
"meta": {
"count": 1
}
}
Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | Invalid request parameters |
| 401 | authentication_required | No API key was supplied |
| 401 | authentication_failed | The supplied API key is not valid |
| 429 | rate_limit_exceeded | Per-second throttle exceeded |
| 429 | quota_exceeded | Monthly quota exhausted |
400 validation_error
{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"details": [
{
"field": "population_min",
"issue": "must be a valid integer"
}
],
"request_id": "req_abc123"
}
}
401 authentication_required
{
"error": {
"code": "authentication_required",
"message": "API key required. Get one at /signup",
"request_id": "req_abc123"
}
}
401 authentication_failed
{
"error": {
"code": "authentication_failed",
"message": "Invalid API key",
"request_id": "req_abc123"
}
}
429 rate_limit_exceeded
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again later.",
"request_id": "req_abc123"
}
}
429 quota_exceeded
{
"error": {
"code": "quota_exceeded",
"message": "Monthly quota exhausted. You've used 2,000,000 of 2,000,000 requests for the current period, which resets on March 1, 2026. Upgrade for a higher limit.",
"request_id": "req_abc123",
"quota": {
"limit": 2000000,
"used": 2000000,
"resets_at": "2026-03-01T00:00:00Z",
"upgrade_url": "https://geosearch.dev/dashboard/billing"
}
}
}
Returns the nearest city for a given latitude/longitude. Uses PostGIS spatial index for fast reverse geocoding.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| lat | number | required | Latitude (-90 to 90) |
| lon | number | required | Longitude (-180 to 180) |
| fields | string | optional | Comma-separated list of fields to include in the response |
Code samples
Response
{
"data": {
"city": {
"id": 5391959,
"name": "San Francisco",
"country_code": "US",
"population": 873965,
"timezone": "America/Los_Angeles",
"latitude": 37.77493,
"longitude": -122.41942,
"distance_km": 0.3
},
"distance_km": 0.3
}
}
Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | Invalid request parameters |
| 401 | authentication_required | No API key was supplied |
| 401 | authentication_failed | The supplied API key is not valid |
| 404 | not_found | Resource not found |
| 429 | rate_limit_exceeded | Per-second throttle exceeded |
| 429 | quota_exceeded | Monthly quota exhausted |
400 validation_error
{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"details": [
{
"field": "population_min",
"issue": "must be a valid integer"
}
],
"request_id": "req_abc123"
}
}
401 authentication_required
{
"error": {
"code": "authentication_required",
"message": "API key required. Get one at /signup",
"request_id": "req_abc123"
}
}
401 authentication_failed
{
"error": {
"code": "authentication_failed",
"message": "Invalid API key",
"request_id": "req_abc123"
}
}
404 not_found
{
"error": {
"code": "not_found",
"message": "The requested resource was not found",
"request_id": "req_abc123"
}
}
429 rate_limit_exceeded
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again later.",
"request_id": "req_abc123"
}
}
429 quota_exceeded
{
"error": {
"code": "quota_exceeded",
"message": "Monthly quota exhausted. You've used 2,000,000 of 2,000,000 requests for the current period, which resets on March 1, 2026. Upgrade for a higher limit.",
"request_id": "req_abc123",
"quota": {
"limit": 2000000,
"used": 2000000,
"resets_at": "2026-03-01T00:00:00Z",
"upgrade_url": "https://geosearch.dev/dashboard/billing"
}
}
}
Resolve coordinates to their containing administrative areas
GET/v1/resolve
Quota cost: 1 unit Try itReturns the administrative areas whose BOUNDARY POLYGONS CONTAIN the given coordinate, ordered country first.
How this differs from /v1/reverse
These two endpoints take the same parameters and answer different questions, and the difference is the reason both exist.
/v1/reverse returns the NEAREST city. It always returns something, and
for a point near a border that something is sometimes in the
neighbouring country.
/v1/resolve returns the areas that actually CONTAIN the point. It is
never wrong about which country a point is in — and it sometimes returns
nothing at all, because no polygon covers the point or because we hold no
polygon for that country. Choose this endpoint when correctness at
borders matters and choose /v1/reverse when you always need an answer.
depth RUNS THE OPPOSITE DIRECTION FROM /v1/cities/{id}/hierarchy
Read this before writing code that consumes both endpoints.
Both return the same node SHAPE — geoname_id, name, type, depth
— so one rendering path can accept either. The depth SEMANTICS are
inverted between them:
- On this endpoint
depthis POSITIONAL, counting outward-in from the largest area:depth: 0is the COUNTRY,depth: 1is the region inside it, and so on. - On
/v1/cities/{id}/hierarchydepthcounts up from the entity that was asked about:depth: 0is the CITY, and the country is at the highest depth in the list.
So data[0] is the country here and the city there. Code that sorts or
indexes on depth across both endpoints without accounting for this will
silently invert the hierarchy rather than fail.
Cost and availability
One quota unit, on every plan including Free. This is a single indexed point-in-polygon probe returning names, not a geometry transfer, so it carries no premium and no tier gate.
?fields= IS NOT SUPPORTED on this endpoint and is ignored if sent.
The four node fields are all small, so selection would save nothing.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| lat | number | required | Latitude (-90 to 90). Must be a finite number: |
| lon | number | required | Longitude (-180 to 180). Must be a finite number; see |
| lang | string | optional | ISO 639-1 language code for localized names (e.g., de, fr, ja) |
Code samples
Response
{
"data": [
{
"geoname_id": 6252001,
"name": "United States",
"type": "country",
"depth": 0
},
{
"geoname_id": 5332921,
"name": "California",
"type": "region",
"depth": 1
}
],
"meta": {
"count": 2
}
}
Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | A missing, unparseable, non-finite or out-of-range `lat` or `lon`. NOTE THE STATUS. Parameter failures on THIS endpoint are 400 `bad_request`, mirroring `/v1/reverse`, whose parameter contract this endpoint deliberately copies so that the generated SDK method reads `resolve(lat, lon)` beside `reverse(lat, lon)`. That is a different convention from `?simplify=` on `/v1/boundaries/{geoname_id}`, which is a 422 `validation_error` with per-field `error.details`. The two are separate conventions on purpose, not an inconsistency to be harmonised away — treat them as two shapes when writing a client. |
| 401 | authentication_required | No API key was supplied |
| 401 | authentication_failed | The supplied API key is not valid |
| 404 | not_found | No administrative area covers the supplied coordinate. EXACTLY ONE CODE, AND THE MESSAGE CLAIMS NOTHING ABOUT WHY. Two distinct situations produce this response — the point is in open water, or it is on land we hold no polygon for — and the server genuinely cannot tell them apart. Reporting a confident cause would be wrong a predictable fraction of the time, so it reports neither. This is not an exotic path and should be handled as a normal outcome: measured over a random 19,558-city sample, 0.70% of cities resolve to no country polygon at all. An empty 200 was rejected: an empty array cannot be told apart from a successful "nothing matched". |
| 429 | rate_limit_exceeded | Per-second throttle exceeded |
| 429 | quota_exceeded | Monthly quota exhausted |
| 503 | area_query_timeout | The point-in-polygon probe exceeded the statement timeout that bounds it. This is a 503 and NOT a 500, deliberately: the server is healthy and the request was valid — this one probe against an unusually complex set of candidate polygons simply ran out of its time budget. It is worth retrying. There is no narrower query to send. This operation takes a single coordinate; there is no `bbox`, no `within` and no filter set to reduce. Retry, and if the failure persists for a particular coordinate, report it — a coordinate that reliably times out is a data problem on our side, not a malformed request on yours. |
400 bad_request
{
"error": {
"code": "bad_request",
"message": "lat must be between -90 and 90",
"request_id": "req_abc123"
}
}
401 authentication_required
{
"error": {
"code": "authentication_required",
"message": "API key required. Get one at /signup",
"request_id": "req_abc123"
}
}
401 authentication_failed
{
"error": {
"code": "authentication_failed",
"message": "Invalid API key",
"request_id": "req_abc123"
}
}
404 not_found
{
"error": {
"code": "not_found",
"message": "administrative area not found: covering these coordinates",
"request_id": "req_abc123"
}
}
429 rate_limit_exceeded
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again later.",
"request_id": "req_abc123"
}
}
429 quota_exceeded
{
"error": {
"code": "quota_exceeded",
"message": "Monthly quota exhausted. You've used 2,000,000 of 2,000,000 requests for the current period, which resets on March 1, 2026. Upgrade for a higher limit.",
"request_id": "req_abc123",
"quota": {
"limit": 2000000,
"used": 2000000,
"resets_at": "2026-03-01T00:00:00Z",
"upgrade_url": "https://geosearch.dev/dashboard/billing"
}
}
}
503 area_query_timeout
{
"error": {
"code": "area_query_timeout",
"message": "The point-in-polygon probe took too long to complete. This is usually temporary — please retry.",
"request_id": "req_abc123"
}
}
Performs a fuzzy text search across countries, regions, and cities using trigram matching. Results are ranked by relevance and population. Uses simple limit pagination (no cursor).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| lang | string | optional | ISO 639-1 language code for localized names (e.g., de, fr, ja) |
| q | string | required | Search query (minimum 2 characters) |
| type | string | optional | Filter by entity type (comma-separated). Allowed: country, region, city. |
| limit | integer | optional | Maximum results to return (1-100, default 25) |
| fields | string | optional | Comma-separated list of fields to include in the response |
Code samples
Response
{
"data": [
{
"type": "city",
"id": 5391959,
"name": "San Francisco",
"rank": 0.95,
"country": "US",
"population": 873965,
"latitude": 37.77493,
"longitude": -122.41942
}
],
"meta": {
"has_next": false,
"has_prev": false,
"count": 1
}
}
Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | Invalid request parameters |
| 401 | authentication_required | No API key was supplied |
| 401 | authentication_failed | The supplied API key is not valid |
| 429 | rate_limit_exceeded | Per-second throttle exceeded |
| 429 | quota_exceeded | Monthly quota exhausted |
400 validation_error
{
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"details": [
{
"field": "population_min",
"issue": "must be a valid integer"
}
],
"request_id": "req_abc123"
}
}
401 authentication_required
{
"error": {
"code": "authentication_required",
"message": "API key required. Get one at /signup",
"request_id": "req_abc123"
}
}
401 authentication_failed
{
"error": {
"code": "authentication_failed",
"message": "Invalid API key",
"request_id": "req_abc123"
}
}
429 rate_limit_exceeded
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again later.",
"request_id": "req_abc123"
}
}
429 quota_exceeded
{
"error": {
"code": "quota_exceeded",
"message": "Monthly quota exhausted. You've used 2,000,000 of 2,000,000 requests for the current period, which resets on March 1, 2026. Upgrade for a higher limit.",
"request_id": "req_abc123",
"quota": {
"limit": 2000000,
"used": 2000000,
"resets_at": "2026-03-01T00:00:00Z",
"upgrade_url": "https://geosearch.dev/dashboard/billing"
}
}
}