snapdata
Datasets

Geo

World (reference data). 250 countries, 5,308 subdivisions and 152,970 cities, with coordinates, timezones, currencies and calling codes. ODbL v1.0.

What this is

The gazetteer: countries, world regions, subdivisions and cities, with the attributes that make each one useful on its own — ISO codes, currencies, international calling codes, postal-code formats, coordinates and IANA timezones.

curl -s "https://snapdata.dev/api/v1/geo/search?q=chennai&country=IN"
EntityRows
Countries250
Regions6
Subregions22
States, provinces and territories5,308
Cities152,970

This dataset is ODbL v1.0, not CC BY 4.0. It is share-alike: if you build a derivative database from it and make that database publicly available, you must offer the derivative under ODbL too. Attribution is required in this exact form:

Data by Countries States Cities Database
https://github.com/dr5hn/countries-states-cities-database | ODbL v1.0

Using the data inside an application — a dropdown, a map, a report — is a Produced Work, which needs the notice but not share-alike. Republishing the database itself is what triggers the copyleft.

Ours is at /api/v1/geo/dump/cities.json and its siblings, which is how we discharge that obligation rather than merely claim to.

Search is accent-insensitive

Names are indexed on a folded key — lower-cased with diacritics stripped — so you can type on an English keyboard and still find the place:

You typeYou get
zurichZürich
sao pauloSão Paulo
cordobaCórdoba
ashkashamAshkāsham

The fold happens at import, in TypeScript, using Unicode normalisation. It cannot be done in SQL: SQLite's lower() is ASCII-only and leaves Ü alone, so a column folded that way would still miss every accented name.

Search matches the start of a name, not the middle. chen finds Chennai; it does not find Kanchenjunga. That is deliberate — a prefix is an indexed range, and a substring search over 152,970 rows is a full scan on a store that bills per row read.

Endpoints

PathWhat it returns
/api/v1/geo/countries.jsonAll 250, full records
/api/v1/geo/{iso2}.jsonOne country and its subdivisions. in, US, either works
/api/v1/geo/dialcodes.jsonEvery calling code, grouped by code
/api/v1/geo/search?q=&country=&limit=City prefix search, accent-folded
/api/v1/geo/near?lat=&lng=&km=Cities within a radius, nearest first
/api/v1/geo/dialcode/{code}Countries on one calling code
/api/v1/geo/cities?state={id}Cities in one subdivision
/api/v1/geo/dump/{table}.jsonThe ODbL derivative, per table

The first three and the dump are R2 objects at permanent URLs. The rest take a parameter you invent, so they are answered from D1 — which is why they are the only endpoints on this API with a database in the request path.

A calling code is not a country

250 countries share 209 codes, so /api/v1/geo/dialcode/{code} returns an array and always will:

CodeCountries
+125 — US, Canada, and 23 Caribbean and Pacific territories
+444 — United Kingdom, Guernsey, Jersey, Isle of Man
+72 — Russia, Kazakhstan
+613 — Australia, Christmas Island, Cocos Islands

A form that stores +1 and calls it the country has silently merged the United States with twenty-four other places. Modelling it as one-to-one is wrong for exactly the users least likely to report it.

Postal-code validation without the postal codes

geo_country carries postal_code_format and postal_code_regex, populated for 189 of 250 countries:

{ "iso2": "IN", "postal_code_format": "######",    "postal_code_regex": "^(\\d{6})$" }
{ "iso2": "US", "postal_code_format": "#####-####", "postal_code_regex": "^\\d{5}(-\\d{4})?$" }

That validates the shape of a ZIP, PIN or postcode without carrying a postal-code table at all — which is most of what a form actually needs.

There are no postal codes in this dataset, and that was a choice. Upstream ships 844,248 of them and we read all of them before deciding. Three things ruled them out for now: city_id is empty on every single row, so there is no postcode-to-city link; coordinates exist in about eight countries, so India, Japan, Germany and France would have lookup but no radius search; and the UK has 124 rows for the entire country. Adding them would have been five times the data for a feature that behaves differently depending on where the user is.

Not a price series

Reference data, so it does not follow the envelope the rest of the API uses. No series/30d windows, no is_trading_day, no status, no dated snapshots. A city has no trading day and no value, and inventing those fields to fit the envelope would be a lie about what the data is. The same reasoning as tickers.

Cadence

Monthly, on the reference tier — see Dates & trading days § 9. Upstream cuts a release every few months, and the release tag is pinned in our importer, so a rebuild is reproducible: the same code on the same release produces the same 158,556 rows.

Each response and each published file carries the release it was built from.

Licence

ODbL v1.0, share-alike, attribution required. This is the only dataset here under a copyleft licence, which is why it is never joined into another dataset's published files — doing so would pull that dataset under share-alike too.

On this page