Documentation navigation

Search API

Cross-type fuzzy text search

Authentication: X-API-Key header on every request. Get a key.

Autocomplete search

GET/v1/autocomplete

Quota cost: 1 unit Try it

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"
    }
  }
}

Reverse geocode coordinates

GET/v1/reverse

Quota cost: 1 unit Try it

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 it

Returns 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 depth is POSITIONAL, counting outward-in from the largest area: depth: 0 is the COUNTRY, depth: 1 is the region inside it, and so on.
  • On /v1/cities/{id}/hierarchy depth counts up from the entity that was asked about: depth: 0 is 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: NaN and Infinity are rejected with a 400 rather than being passed to the spatial index, which would answer them with an ordinary "not found".

lon number required

Longitude (-180 to 180). Must be a finite number; see lat.

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"
  }
}