# Search API — GeoSearch

> The GeoSearch Search API: 4 endpoints with parameters, quota costs, response bodies and code samples in six languages.

Base URL: https://geosearch.dev · Auth: X-API-Key header on every request.
HTML version: https://geosearch.dev/docs/api/search

Cross-type fuzzy text search

## Autocomplete search

`GET /v1/autocomplete` · Quota cost: 1 unit

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

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/autocomplete?q=San+Fran&limit=10"
```

#### Go

```go
// go get github.com/geosearch-dev/geosearch-go
client := geosearch.NewAPIClient(geosearch.NewConfiguration())
ctx := context.WithValue(context.Background(), geosearch.ContextAPIKeys,
    map[string]geosearch.APIKey{"apiKeyAuth": {Key: "YOUR_KEY"}})
suggestions, _, err := client.SearchAPI.Autocomplete(ctx).
    Q("San Fran").Limit(10).Execute()
```

#### Python

```python
# pip install git+https://github.com/geosearch-dev/geosearch-python.git
import geosearch

cfg = geosearch.Configuration(api_key={"apiKeyAuth": "YOUR_KEY"})
with geosearch.ApiClient(cfg) as client:
    suggestions = geosearch.SearchApi(client).autocomplete("San Fran", limit=10).data
```

#### TypeScript

```typescript
// npm install github:geosearch-dev/geosearch-typescript
import { Configuration, SearchApi } from "@geosearch/client";

const api = new SearchApi(new Configuration({ apiKey: "YOUR_KEY" }));
const suggestions = (await api.autocomplete({ q: "San Fran", limit: 10 })).data;
```

#### Ruby

```ruby
# gem 'geosearch', git: 'https://github.com/geosearch-dev/geosearch-ruby.git'
require "geosearch"

GeoSearch.configure { |c| c.api_key["X-API-Key"] = "YOUR_KEY" }
suggestions = GeoSearch::SearchApi.new.autocomplete("San Fran", limit: 10).data
```

#### PHP

```php
// composer config repositories.geosearch vcs https://github.com/geosearch-dev/geosearch-php.git
// composer require geosearch-dev/geosearch-php
$cfg = GeoSearch\Configuration::getDefaultConfiguration()
    ->setApiKey("X-API-Key", "YOUR_KEY");
$suggestions = (new GeoSearch\Api\SearchApi(null, $cfg))->autocomplete("San Fran", null, 10)->getData();
// q, lang, limit — fields is the one remaining optional parameter
```

### Response

```json
{
  "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 |

## Reverse geocode coordinates

`GET /v1/reverse` · Quota cost: 1 unit

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

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/reverse?lat=37.7749&lon=-122.4194"
```

#### Go

```go
// go get github.com/geosearch-dev/geosearch-go
client := geosearch.NewAPIClient(geosearch.NewConfiguration())
ctx := context.WithValue(context.Background(), geosearch.ContextAPIKeys,
    map[string]geosearch.APIKey{"apiKeyAuth": {Key: "YOUR_KEY"}})
nearest, _, err := client.SearchAPI.ReverseGeocode(ctx).
    Lat(37.7749).Lon(-122.4194).Execute()
```

#### Python

```python
# pip install git+https://github.com/geosearch-dev/geosearch-python.git
import geosearch

cfg = geosearch.Configuration(api_key={"apiKeyAuth": "YOUR_KEY"})
with geosearch.ApiClient(cfg) as client:
    nearest = geosearch.SearchApi(client).reverse_geocode(37.7749, -122.4194).data
```

#### TypeScript

```typescript
// npm install github:geosearch-dev/geosearch-typescript
import { Configuration, SearchApi } from "@geosearch/client";

const api = new SearchApi(new Configuration({ apiKey: "YOUR_KEY" }));
const nearest = (await api.reverseGeocode({ lat: 37.7749, lon: -122.4194 })).data;
```

#### Ruby

```ruby
# gem 'geosearch', git: 'https://github.com/geosearch-dev/geosearch-ruby.git'
require "geosearch"

GeoSearch.configure { |c| c.api_key["X-API-Key"] = "YOUR_KEY" }
nearest = GeoSearch::SearchApi.new.reverse_geocode(37.7749, -122.4194).data
```

#### PHP

```php
// composer config repositories.geosearch vcs https://github.com/geosearch-dev/geosearch-php.git
// composer require geosearch-dev/geosearch-php
$cfg = GeoSearch\Configuration::getDefaultConfiguration()
    ->setApiKey("X-API-Key", "YOUR_KEY");
$nearest = (new GeoSearch\Api\SearchApi(null, $cfg))->reverseGeocode(37.7749, -122.4194)->getData();
```

### Response

```json
{
  "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 |

## Resolve coordinates to their containing administrative areas

`GET /v1/resolve` · Quota cost: 1 unit

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

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/resolve?lat=37.7749&lon=-122.4194"
```

#### Go

```go
// go get github.com/geosearch-dev/geosearch-go
client := geosearch.NewAPIClient(geosearch.NewConfiguration())
ctx := context.WithValue(context.Background(), geosearch.ContextAPIKeys,
    map[string]geosearch.APIKey{"apiKeyAuth": {Key: "YOUR_KEY"}})
areas, _, err := client.SearchAPI.ResolveCoordinate(ctx).
    Lat(37.7749).Lon(-122.4194).Execute()
```

#### Python

```python
# pip install git+https://github.com/geosearch-dev/geosearch-python.git
import geosearch

cfg = geosearch.Configuration(api_key={"apiKeyAuth": "YOUR_KEY"})
with geosearch.ApiClient(cfg) as client:
    areas = geosearch.SearchApi(client).resolve_coordinate(37.7749, -122.4194).data
```

#### TypeScript

```typescript
// npm install github:geosearch-dev/geosearch-typescript
import { Configuration, SearchApi } from "@geosearch/client";

const api = new SearchApi(new Configuration({ apiKey: "YOUR_KEY" }));
const areas = (await api.resolveCoordinate({ lat: 37.7749, lon: -122.4194 })).data;
```

#### Ruby

```ruby
# gem 'geosearch', git: 'https://github.com/geosearch-dev/geosearch-ruby.git'
require "geosearch"

GeoSearch.configure { |c| c.api_key["X-API-Key"] = "YOUR_KEY" }
areas = GeoSearch::SearchApi.new.resolve_coordinate(37.7749, -122.4194).data
```

#### PHP

```php
// composer config repositories.geosearch vcs https://github.com/geosearch-dev/geosearch-php.git
// composer require geosearch-dev/geosearch-php
$cfg = GeoSearch\Configuration::getDefaultConfiguration()
    ->setApiKey("X-API-Key", "YOUR_KEY");
$areas = (new GeoSearch\Api\SearchApi(null, $cfg))->resolveCoordinate(37.7749, -122.4194)->getData();
```

### Response

```json
{
  "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. |

## Cross-type search

`GET /v1/search` · Quota cost: 1 unit

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

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/search?q=San+Fran&type=city&limit=10"
```

#### Go

```go
// go get github.com/geosearch-dev/geosearch-go
client := geosearch.NewAPIClient(geosearch.NewConfiguration())
ctx := context.WithValue(context.Background(), geosearch.ContextAPIKeys,
    map[string]geosearch.APIKey{"apiKeyAuth": {Key: "YOUR_KEY"}})
results, _, err := client.SearchAPI.Search(ctx).
    Q("San Fran").Type_("city").Limit(10).Execute()
```

#### Python

```python
# pip install git+https://github.com/geosearch-dev/geosearch-python.git
import geosearch

cfg = geosearch.Configuration(api_key={"apiKeyAuth": "YOUR_KEY"})
with geosearch.ApiClient(cfg) as client:
    results = geosearch.SearchApi(client).search("San Fran", type="city", limit=10).data
```

#### TypeScript

```typescript
// npm install github:geosearch-dev/geosearch-typescript
import { Configuration, SearchApi } from "@geosearch/client";

const api = new SearchApi(new Configuration({ apiKey: "YOUR_KEY" }));
const results = (await api.search({ q: "San Fran", type: "city", limit: 10 })).data;
```

#### Ruby

```ruby
# gem 'geosearch', git: 'https://github.com/geosearch-dev/geosearch-ruby.git'
require "geosearch"

GeoSearch.configure { |c| c.api_key["X-API-Key"] = "YOUR_KEY" }
results = GeoSearch::SearchApi.new.search("San Fran", type: "city", limit: 10).data
```

#### PHP

```php
// composer config repositories.geosearch vcs https://github.com/geosearch-dev/geosearch-php.git
// composer require geosearch-dev/geosearch-php
$cfg = GeoSearch\Configuration::getDefaultConfiguration()
    ->setApiKey("X-API-Key", "YOUR_KEY");
$results = (new GeoSearch\Api\SearchApi(null, $cfg))->search("San Fran", null, "city", 10)->getData();
// q, lang, type, limit shown — fields is the one remaining optional parameter
```

### Response

```json
{
  "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 |

