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"| Entity | Rows |
|---|---|
| Countries | 250 |
| Regions | 6 |
| Subregions | 22 |
| States, provinces and territories | 5,308 |
| Cities | 152,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.0Using 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 type | You get |
|---|---|
zurich | Zürich |
sao paulo | São Paulo |
cordoba | Córdoba |
ashkasham | Ashkā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
| Path | What it returns |
|---|---|
/api/v1/geo/countries.json | All 250, full records |
/api/v1/geo/{iso2}.json | One country and its subdivisions. in, US, either works |
/api/v1/geo/dialcodes.json | Every 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}.json | The 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:
| Code | Countries |
|---|---|
+1 | 25 — US, Canada, and 23 Caribbean and Pacific territories |
+44 | 4 — United Kingdom, Guernsey, Jersey, Isle of Man |
+7 | 2 — Russia, Kazakhstan |
+61 | 3 — 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.