# Cities API — GeoSearch

> The GeoSearch Cities 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/cities

City data with geographic coordinates

## List cities

`GET /v1/cities` · Quota cost: 1 unit — 2 with ?within=

Returns a paginated list of cities with optional filtering by country, admin code, name, population, timezone, and elevation.

### Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| lang | string | optional | ISO 639-1 language code for localized names (e.g., de, fr, ja) |
| country | string | optional | Filter by ISO alpha-2 country codes (comma-separated) |
| admin1 | string | optional | Filter by admin1 code (state/province) |
| name | string | optional | Filter by city name (trigram fuzzy search) |
| population_min | integer | optional | Minimum population filter |
| population_max | integer | optional | Maximum population filter |
| timezone | string | optional | Filter by IANA timezone ID |
| min_elevation | integer | optional | Minimum elevation in meters |
| max_elevation | integer | optional | Maximum elevation in meters |
| within | integer | optional | Return only results whose coordinates fall geometrically inside the boundary of the given area, identified by its GeoNames id. Countries and administrative regions are valid areas; a city id is not. Accepts exactly one id. A COMMA-SEPARATED LIST IS REJECTED with a 422 naming the limit — a deliberate departure from the comma-separated convention `country` uses, because each value would be a separate polygon intersection. Ask for one area per request. COST: a request using this parameter consumes 2 quota units instead of 1, on every plan including Free. Combining it with `bbox` still costs 2, not 3 — the highest multiplier applies rather than the sum, and adding a box makes the query cheaper to serve, so it is never penalised. This asks a GEOMETRIC question and can therefore return a different set of cities than `/v1/regions/{id}/cities`, which asks an administrative one. See that endpoint's description for when and why the two disagree. Two failures are reported with distinct 400 codes so a typo is distinguishable from a coverage gap: `area_not_an_area` means the id does not name a country or region at all, and `area_no_boundary` means it does but no boundary polygon is available for it yet. |
| bbox | string | optional | Return only results inside the bounding box, given as four comma-separated numbers in the order `w,s,e,n` — west longitude, south latitude, east longitude, north latitude. Longitudes must be within [-180, 180] and latitudes within [-90, 90]. A box where WEST IS GREATER THAN EAST wraps the antimeridian and is fully supported: `bbox=170,-20,-170,-10` is a box around Fiji, evaluated as the union of the two halves it spans. Latitude has no equivalent wrap-around meaning, so `s` greater than `n` is a validation error rather than a wrapped box. Charged at the standard request cost of 1 unit. Adding it to a `within` query narrows the candidate set before the polygon test and does not raise the charge. |
| cursor | string | optional | Pagination cursor from a previous response |
| limit | integer | optional | Number of results per page (1-100, default 25) |
| fields | string | optional | Comma-separated list of fields to include in the response |
| sort | string | optional | Sort field. Allowed: name, population, elevation, id. |

### Code samples

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/cities?country=US&population_min=100000&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"}})
cities, _, err := client.CitiesAPI.ListCities(ctx).
    Country("US").PopulationMin(100000).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:
    cities = geosearch.CitiesApi(client).list_cities(
        country="US", population_min=100000, limit=10).data
```

#### TypeScript

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

const api = new CitiesApi(new Configuration({ apiKey: "YOUR_KEY" }));
const cities = (await api.listCities({ country: "US", populationMin: 100000, 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" }
cities = GeoSearch::CitiesApi.new.list_cities(country: "US", population_min: 100000, 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");
$cities = (new GeoSearch\Api\CitiesApi(null, $cfg))->listCities(null, "US");
// lang, country shown — the remaining 13 parameters are positional and optional
```

### Response

```json
{
  "data": [
    {
      "id": 5391959,
      "geoname_id": 5391959,
      "name": "San Francisco",
      "ascii_name": "San Francisco",
      "country_code": "US",
      "admin1_code": "CA",
      "population": 873965,
      "timezone": "America/Los_Angeles",
      "latitude": 37.77493,
      "longitude": -122.41942
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6NTM5MTk2MH0",
    "has_next": true,
    "has_prev": false,
    "count": 25
  }
}
```

### Errors

| Status | Code | When |
|--------|------|------|
| 400 | area_not_an_area | The within id is not an area |
| 400 | area_no_boundary | No boundary polygon is held for that area |
| 400 | bad_request | A malformed cursor, sort or pagination value |
| 401 | authentication_required | No API key was supplied |
| 401 | authentication_failed | The supplied API key is not valid |
| 422 | validation_error | More than one area requested |
| 422 | validation_error | A bounding box whose south edge is north of its north edge |
| 429 | rate_limit_exceeded | Per-second throttle exceeded |
| 429 | quota_exceeded | Monthly quota exhausted |
| 503 | area_query_timeout | The containment query 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 query against an unusually large or complex boundary simply ran out of its time budget. It is therefore worth retrying, and worth retrying with a narrower query. Adding `bbox` or further filters alongside `within` reduces the candidate set before the polygon test and is the most effective remedy. THIS RESPONSE BELONGS TO THE `within=` ROUTES ONLY. `GET /v1/resolve` and `GET /v1/boundaries/{geoname_id}` have their own 503 components (`ResolveQueryTimeout` and `BoundaryQueryTimeout`) because the remedy above is false on both: neither accepts `bbox` or `within`. |

## Get city by ID

`GET /v1/cities/{id}` · Quota cost: 1 unit

Returns a single city by its numeric ID.

### Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| id | integer | required | City ID |
| lang | string | optional | ISO 639-1 language code for localized names (e.g., de, fr, ja) |
| 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/cities/5391959
```

#### 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"}})
city, _, err := client.CitiesAPI.GetCity(ctx, 5391959).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:
    city = geosearch.CitiesApi(client).get_city(5391959).data
```

#### TypeScript

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

const api = new CitiesApi(new Configuration({ apiKey: "YOUR_KEY" }));
const city = (await api.getCity({ id: 5391959 })).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" }
city = GeoSearch::CitiesApi.new.get_city(5391959).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");
$city = (new GeoSearch\Api\CitiesApi(null, $cfg))->getCity(5391959)->getData();
```

### Response

```json
{
  "data": {
    "id": 5391959,
    "geoname_id": 5391959,
    "name": "San Francisco",
    "ascii_name": "San Francisco",
    "country_code": "US",
    "admin1_code": "CA",
    "admin2_code": "075",
    "population": 873965,
    "elevation": 16,
    "timezone": "America/Los_Angeles",
    "latitude": 37.77493,
    "longitude": -122.41942,
    "country": {
      "iso_code": "US",
      "name": "United States"
    },
    "region": {
      "id": 5332921,
      "name": "California",
      "admin_code": "CA"
    }
  }
}
```

### 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 |

## Get administrative hierarchy for a city

`GET /v1/cities/{id}/hierarchy` · Quota cost: 1 unit

Returns the full administrative hierarchy for a city, ordered from
the city itself up through region, country, and continent.

### Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| id | integer | required | City ID |
| 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/cities/5391959/hierarchy"
```

#### 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"}})
hierarchy, _, err := client.CitiesAPI.CityHierarchy(ctx, 5391959).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:
    hierarchy = geosearch.CitiesApi(client).city_hierarchy(5391959).data
```

#### TypeScript

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

const api = new CitiesApi(new Configuration({ apiKey: "YOUR_KEY" }));
const hierarchy = (await api.cityHierarchy({ id: 5391959 })).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" }
hierarchy = GeoSearch::CitiesApi.new.city_hierarchy(5391959).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");
$hierarchy = (new GeoSearch\Api\CitiesApi(null, $cfg))->cityHierarchy(5391959)->getData();
```

### Response

```json
{
  "data": [
    {
      "geoname_id": 5391959,
      "name": "San Francisco",
      "type": "city",
      "depth": 0
    },
    {
      "geoname_id": 5332921,
      "name": "California",
      "type": "region",
      "depth": 1
    },
    {
      "geoname_id": 6252001,
      "name": "United States",
      "type": "country",
      "depth": 2
    },
    {
      "geoname_id": 6255149,
      "name": "North America",
      "type": "continent",
      "depth": 3
    }
  ],
  "meta": {
    "count": 4
  }
}
```

### Errors

| Status | Code | When |
|--------|------|------|
| 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 |

## Find nearby cities

`GET /v1/cities/nearby` · Quota cost: 1 unit

Returns cities near a given latitude/longitude within a specified radius.
Results are ordered by distance. Uses PostGIS spatial index for fast lookups.

### Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| lat | number | required | Latitude (-90 to 90) |
| lon | number | required | Longitude (-180 to 180) |
| radius | number | optional | Search radius in kilometers (default 50, max 200) |
| limit | integer | optional | Maximum results to return (1-250, 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/cities/nearby?lat=37.7749&lon=-122.4194&radius=50"
```

#### 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"}})
cities, _, err := client.CitiesAPI.NearbyCities(ctx).
    Lat(37.7749).Lon(-122.4194).Radius(50).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:
    cities = geosearch.CitiesApi(client).nearby_cities(37.7749, -122.4194, radius=50).data
```

#### TypeScript

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

const api = new CitiesApi(new Configuration({ apiKey: "YOUR_KEY" }));
const cities = (await api.nearbyCities({ lat: 37.7749, lon: -122.4194, radius: 50 })).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" }
cities = GeoSearch::CitiesApi.new.nearby_cities(37.7749, -122.4194, radius: 50).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");
$cities = (new GeoSearch\Api\CitiesApi(null, $cfg))->nearbyCities(37.7749, -122.4194, 50)->getData();
```

### Response

```json
{
  "data": [
    {
      "id": 5391959,
      "name": "San Francisco",
      "country_code": "US",
      "population": 873965,
      "timezone": "America/Los_Angeles",
      "latitude": 37.77493,
      "longitude": -122.41942,
      "distance_km": 0.5
    }
  ],
  "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 |

