placedb

Endpoints

Four files and a rule for walking them.

There is no key, no account and no rate limit. Every path below is a static object behind a CDN, so a request costs you a fetch and costs this service nothing.

Calling it

Base URL

https://api.placedb.org

Authentication
None. There is no key to send and no header to set.
From a browser
Allowed from any origin for GET and HEAD, so you can call it directly from client code with no proxy.
Caching
Data files are served with max-age=86400; index.json uses max-age=60 because it is the record of what is published. Files are replaced only when the dataset publishes a new release, and changed files are evicted from the edge at that point.
Missing paths
A 404 is an answer, not an error — see the autocomplete walk below, where it means the level above was already complete.

Countries

Every country, in one file

GET /v1/countries.json

255 countries and territories with ISO codes, calling codes, currency, capital, flag, and the number of divisions and settlements each one contains. Around 105 KB, so it is reasonable to fetch once and keep.

{"countries":[{"calling_code":["+247"],"capital_name":"Georgetown",
"code":"AC","flag_emoji":"🇦🇨","id":31890709,"iso_3166_1_alpha3":"ASC",
"name":"Ascension","official_language":"en","regions":2,
"settlements":4…
First 210 bytes of the live response

Divisions

Administrative divisions of one country

GET /v1/regions/<CC>.json

<CC> is the ISO 3166-1 alpha-2 code from countries.json. Each division carries its id, which is the key you need for the settlements file, and its own settlement count.

{"country":"IS","regions":[{"id":203304,"iso_3166_2":"IS-1",
"lat":64.2,"lon":-21.7,"name":"Capital Region","population":233034,
"settlements":16,"slug":"capital-region"},{"id":220663,
"iso_3166_2":"IS-7","lat":65…
First 210 bytes of the live response

Settlements

Every settlement inside one division

GET /v1/cities/<CC>/<admin1>.json

<admin1> is a division id from the regions file. This is the complete list — it is never ranked, never truncated, and it is where you go when you need all of them rather than the ones worth suggesting.

{"country":"AC","region":46197,"release":"2026-08-15T0426Z",
"s_version":"ed3ae9dd3630d619","settlements":[]}
First 210 bytes of the live response

The path says cities and the data says settlement. The dataset's own word is the accurate one: these are populated places, not only cities.

Autocomplete

Ask for the prefix you have

GET /v1/ac/<CC>/<prefix>.json

Autocomplete is a cached file and a client-side filter, not a search engine. Normalise what the user typed, ask for it, and go one character deeper only while the answer tells you it is holding back.

The rule

  1. Lowercase the query, strip accents, and drop everything that is not a letter or a digit. San José becomes sanjose.
  2. Fetch the bucket for the first character. If it comes back "complete": true, it holds every place with that prefix — filter it locally and stop.
  3. If it comes back "complete": false, it holds the most populous places only, and total tells you how many exist. Fetch one character deeper.
  4. A 404 means no place in that country carries the prefix, or the level above already answered in full. Either way, stop.

Worked example

/v1/ac/NP/k.json complete · 216 places · Kathmandu is in it

/v1/ac/NP/ka.json 404 — the walk already ended at one character

/v1/ac/DE/ber.json partial · 827 carry the prefix, the ranked head is returned

Nepal never needs a second character; Germany does. The depth is decided per bucket when the tree is built, not per country, which is why you read complete instead of guessing a prefix length.

{"complete":true,"country":"NP","places":[{"id":25104008,
"lat":26.666667,"lon":87.333333,"name":"Koshi Province",
"population":4961412,"type":"region"},{"id":25104017,"lat":29.27,
"lon":82.18,"name":"Karnali Prov…
First 210 bytes of the live response

In code

const key = (s) =>
  s.toLowerCase().normalize('NFD')
   .replace(/\p{Diacritic}/gu, '')
   .replace(/[^a-z0-9]/g, '');

async function suggest(cc, query) {
  const k = key(query);
  let best = null;

  for (let depth = 1; depth <= k.length; depth++) {
    const res = await fetch(
      `https://api.placedb.org/v1/ac/${cc}/${k.slice(0, depth)}.json`
    );
    if (!res.ok) break;          // 404: nothing deeper to ask for
    best = await res.json();
    if (best.complete) break;    // it is holding nothing back
  }

  return (best?.places ?? []).filter(
    (p) => key(p.name).startsWith(k)
  );
}

Conventions

Things that hold everywhere

Absent means absent
A key with no value is omitted rather than sent as null. Most settlements have no population figure, and across 1,674,947 rows that omission is most of the payload.
Coordinates are numbers
lat and lon, not strings. Every settlement has both — that is a guarantee of the dataset, not a tendency.
Names are ASCII
Reykjavík is served as Reykjavik. The transliteration comes from the dataset and this service does not alter it.
A place can be both
Berlin is a division and a settlement, under the same id. Autocomplete returns both, tagged with type. They are different answers; disambiguate rather than deduplicate.
Every file names its release
Responses carry release and s_version, so you can always tell which publication you were handed.

Verifying

Check what you were served

GET /v1/index.json

The index lists every file in the tree with its byte length and sha256, alongside the release it was built from. It is around 3.9 MB, so fetch it deliberately rather than from a page.

$ curl -s https://api.placedb.org/v1/regions/IS.json | sha256sum
$ curl -s https://api.placedb.org/v1/index.json \
    | jq -r '.files["regions/IS.json"].sha256'

The tree is generated from a published release by a deterministic build, so the same release and the same code produce identical bytes. You can regenerate it yourself and get the same hashes.

Leaving

Take the data and go

This service sells not having to host it — nothing more. The dataset is CC0 and published in full, so if this endpoint becomes inconvenient, slow, or gone, you can pull the release and serve it yourself. Nothing about that is a violation of anything.

$ curl -s https://geo.mindstellar.com/releases/latest.json
$ # then fetch releases/<version>/manifest.json and the files it lists

The pipeline that builds it is public at mindstellar/location-data. Current release 2026-08-15T0426Z, CC0-1.0.