# Thai address dataset Thailand's provinces, districts and subdistricts, with their codes, Thai and English names, history and postal codes. The data comes from official sources: - DOPA (the Department of Provincial Administration); - Thailand Post; - MDES's post-office dataset; - ISO 3166-2. It is published as immutable, versioned snapshots at `https://data.farzai.com/thailand-address/`. ## Where the files are ``` https://data.farzai.com/thailand-address/v1/snapshots.json every snapshot, published or withdrawn https://data.farzai.com/thailand-address/v1/latest.json the newest published snapshot (for humans) https://data.farzai.com/thailand-address/v1/{snapshot}/provinces.json https://data.farzai.com/thailand-address/v1/{snapshot}/districts.json https://data.farzai.com/thailand-address/v1/{snapshot}/subdistricts.json https://data.farzai.com/thailand-address/v1/{snapshot}/postal_codes.json https://data.farzai.com/thailand-address/v1/{snapshot}/changes.json https://data.farzai.com/thailand-address/v1/{snapshot}/thailand-address.csv https://data.farzai.com/thailand-address/v1/{snapshot}/schema.json https://data.farzai.com/thailand-address/v1/{snapshot}/manifest.json https://data.farzai.com/thailand-address/v1/{snapshot}/index/postal_codes.json postal code -> subdistricts https://data.farzai.com/thailand-address/v1/{snapshot}/by-province/{code}.json one province, e.g. by-province/10.json ``` For a browser or app that should not download everything: - **`index/postal_codes.json`:** one row per postal code, with its subdistricts and each link's kind, coverage and status. - **`by-province/{code}.json`:** one province, with its districts, subdistricts and postal links. Abolished areas are included, with their status. In this file `data` is an object, `{province, districts, subdistricts, postal_codes}`. Both are derived from the main files, listed in the manifest with their sha256, and checked by the publish gates to match exactly. The files are served with CORS (GET and HEAD, any origin), so a web page can fetch them directly. **Snapshot ids** - `{snapshot}` is the UTC publish date, for example `2026-10-14`. A same-day republish adds `.1`, `.2` and so on. - A snapshot's files never change once published, and are cached for a year. - `latest.json` is cached for 5 minutes. - Pin a snapshot id in code and in any package that bundles the data. Never build on `latest.json`; it is for people and for discovery. **Withdrawn snapshots.** `snapshots.json` lists every snapshot ever uploaded, with `status` `published` or `withdrawn`. A withdrawn snapshot stays reachable at its URL, but must not be used. **Integrity.** `manifest.json` gives the sha256 and size of every file in the snapshot. `latest.json` and `snapshots.json` give the sha256 of each manifest, so a download can be verified end to end. ## File shape Every data file is an envelope: ```json {"schema_version": "1.0.0", "snapshot": "2026-10-14", "data": [ ... ]} ``` A single file fetched on its own still says which schema and snapshot it belongs to. `schema.json` is a JSON Schema (2020-12) that describes every field, with an example for each. The pipeline validates every file against it before publishing. ### Conventions - **Codes are strings:** DOPA codes (`"10"`, `"1001"`, `"100101"`) and postal codes (`"10200"`). Keep them as strings; leading zeros and fixed widths matter. - **Codes are never reused** for a different area. A code is permanent: an area that ends is marked `abolished`, and its code is never given to another area. - **A move gets a new code.** An area that moves to another province or district gets a new code. The old one is abolished with `replaced_by`, and the new one is `added`. There is no "moved". - **null means unknown:** never "empty", and never "since the beginning". `effective_from` is null for almost every area, because DOPA does not publish it in bulk. - **Successors:** `replaced_by` is `[]` for an active area, which nothing has replaced. For an abolished area it is the list of codes, at the same level, of the areas that replaced it; `[]` when it is known that nothing replaced it; or null when the successors are unknown. DOPA does not publish successors, so today every abolished area has null. When a successor becomes known later, `changes.json` has a `replaced_by_changed` change. - **Additions within v1:** new fields, name languages, change kinds and enum values may be added without a new path. Ignore change kinds and enum values you do not know, as you ignore unknown fields. - **Missing English names:** `name.en` is null when DOPA has no romanisation. Do not transliterate one yourself. - **Formats:** dates are ISO 8601 (`YYYY-MM-DD`). Enums are lowercase. - **Names:** names are objects keyed by BCP 47 language tag, `{"th": ..., "en": ...}`. The English is RTGS romanisation, as DOPA publishes it. More languages may be added; ignore language tags you do not know. - **Prefixes:** names are stored without their administrative prefix. Use `kind` and the table below to add one. ### Kinds and their prefixes | `kind` | Thai prefix | English form | |---|---|---| | `province` | จังหวัด | ... Province | | `special_administrative_area` | (none) | Bangkok | | `amphoe` | อำเภอ | ... District | | `khet` | เขต | Khet ... | | `tambon` | ตำบล | ... Subdistrict | | `khwaeng` | แขวง | Khwaeng ... | Bangkok (`10`) is the only `special_administrative_area`. Its districts are `khet` and its subdistricts `khwaeng`. ### The CSV `thailand-address.csv` has one row per active subdistrict and postal code. A subdistrict with no known postal code has one row with the postal columns empty. - Read the CSV by header name, never by column position. New columns are only ever appended. - An empty cell is null. - It is UTF-8, without a byte-order mark. ### Postal codes `postal_codes.json` links postal codes to subdistricts, many to many. - **`kind: facility`:** the code of a mail-processing facility, for example 10001 at Suvarnabhumi Mail Center, not of the places around it. - **`coverage: full`:** the code is the subdistrict's only standard code. - **`coverage: partial`:** the subdistrict has several codes. Some tambon are split between codes by village, so do not assume a partial code is valid for every address in the subdistrict. - **Postal areas:** a postal code's first two digits are a postal area, which is not the DOPA province code. Samut Prakan, DOPA `11`, uses `10xxx`. - **`note`:** an object keyed by language like `name` (`{"th": ..., "en": ...}`), or null for an ordinary link. A facility code's note is the facility's name, for example `{"th": "ศูนย์ไปรษณีย์สุวรรณภูมิ", "en": "Suvarnabhumi Mail Center"}`. A language that no source gives is null; nothing is translated. - **Cross-province codes:** a few subdistricts are served by a post office in the next province. The link's `note.en` says so, and `note.th` is null. - **History:** links are never removed. A link the sources stop giving stays in the file with `status: abolished` and `abolished_on`, the date of the snapshot that first lacked it. **Filter on `status: active`** to get the codes in use. ### Versioning - **Minor `schema_version` (1.x):** adds fields or files. Ignore fields you do not know. - **v2:** renaming, removing or changing the meaning of a field means a new path, `v2`. `v1` stays for at least 12 months after `v2` ships. ## Common tasks - **Validate an address (province, district, subdistrict, postal code):** 1. Look up the subdistrict by code, and check that its `district_code` and `province_code` are the ones given. 2. Check that the postal code has an `active` link to that subdistrict in `postal_codes.json`. 3. Prefer `status: active` areas for new addresses. - **Fill a postal code from a subdistrict:** - If exactly one `active` `standard` link has `coverage: full`, use it. - If there are several, ask the user to choose. Do not pick one. - **Find the areas a postal code serves:** filter `postal_codes.json` by `postal_code` and `status: active`, then look up each `subdistrict_code`. - **Show a full Thai name:** prefix each level's `name.th` with the Thai prefix for its `kind`. For example, แขวงพระบรมมหาราชวัง เขตพระนคร กรุงเทพมหานคร. - **Keep a local copy current:** - Read `snapshots.json`, pick the newest `published` id, and download its files. - Check each file's sha256 against that snapshot's `manifest.json`. - Rebuild your state from the history fields of the newest snapshot. Do not chain `changes.json` files across skipped snapshots. - **See what changed:** `changes.json` lists `added`, `renamed`, `abolished`, `postal_codes_changed` and `replaced_by_changed` against the previous published snapshot, named in `manifest.previous_snapshot`. Each kind has a fixed `before`/`after` shape. ## Do not - Never guess a postal code from a district code, a province code or a nearby subdistrict. Use only the links in `postal_codes.json`. - Never key on row ids or row position in a file or the CSV. Key on codes. - Never build on `latest.json`. Pin a snapshot id. - Never treat null as "none" or "always". It means unknown. - Never transliterate a missing English name. - Never reuse an abolished code, or infer that one was reused. - Never assume a `partial` postal code covers a whole subdistrict. - Never use a `withdrawn` snapshot. ## Sources, licences and attribution The dataset is built from these sources. Each snapshot's `manifest.json` records when each was fetched, its sha256, the date it states its data is as of, and its licence. - **MDES:** the post-office data is published under Creative Commons Attribution. Attribute it as below. - **DOPA and Thailand Post:** no licence is stated. The dataset owner knowingly publishes data derived from them, with the attribution below. - **ISO codes:** the ISO 3166-2 codes come from Debian's iso-codes project. - **DOPA ทำเนียบท้องที่ (ccaatt)** (Department of Provincial Administration (DOPA), Ministry of Interior), . Licence: no licence stated. Used for: codes, Thai and English names, abolition dates. Attribution: Department of Provincial Administration (DOPA), ทำเนียบท้องที่. No licence is stated on the download page; published government record data. - **Thailand Post track web: getSubdistrict** (Thailand Post Co., Ltd.), . Licence: no licence stated. Used for: postal codes, joined by subdistrict code. Attribution: Thailand Post Co., Ltd. No licence is stated; terms: https://postbase.thailandpost.co.th/th/terms-and-conditions. Thailand Post is the authority on postal codes. - **สถานที่ทำการไปรษณีย์ (post offices)** (Office of the Permanent Secretary, Ministry of Digital Economy and Society (MDES)), . Licence: CC-BY. Used for: postal codes, joined by province, district and subdistrict name. Attribution: Ministry of Digital Economy and Society (MDES), dataset "สถานที่ทำการไปรษณีย์" on data.go.th, Creative Commons Attribution (version not stated). - **iso-codes iso_3166-2.json (ISO 3166-2 subdivision codes)** (Debian iso-codes project), . Licence: LGPL-2.1-or-later. Used for: iso_code of each province. Attribution: Debian iso-codes v4.20.1 (LGPL-2.1-or-later); the codes themselves are ISO 3166-2:TH. DOPA's file includes areas abolished as far back as 2006. They are kept with `status: abolished` and their abolition date. DOPA dates 1,456 of them (66 districts, 1,390 subdistricts) 1 January 2549 BE (2006-01-01); no other date holds more than 61. That is a bulk value from when the register was digitised, so those rows have `abolished_on: null` (unknown). The raw value stays in the repository's `sources/`. One abolished row, `970596`, is left out because its parent district and province do not exist in DOPA's file. ## Field reference Generated from `schema.json` (Every file of one snapshot of the Thai address dataset (provinces, districts, subdistricts, postal codes, changes, manifest), plus v1/latest.json and v1/snapshots.json. Each data file is an envelope {schema_version, snapshot, data}; data is a list, except in by-province/.json, where it is one province's object. Consumers must ignore fields, name languages, change kinds and enum values they do not know: adding any of them is a minor schema_version change within v1. Renaming, removing or changing the meaning of a field is v2. Codes are strings and are never reused for a different area. null means unknown, never empty and never 'since the beginning'. Dates are ISO 8601 (YYYY-MM-DD). Enums are lowercase.) ### provinces.json, each row One province, or Bangkok. | field | type | description | example | |---|---|---|---| | `code` | string | DOPA province code: 2 digits, as a string. | `"10"` | | `iso_code` | string or null | ISO 3166-2:TH code; null when ISO has none. | `"TH-10"` | | `name` | object | Names by BCP 47 language tag, without the administrative prefix (see kind for the prefix table). Consumers must ignore language tags they do not know; more may be added in v1. | | | `kind` | one of `province`, `special_administrative_area` | province (Thai prefix จังหวัด, English 'Province'), or special_administrative_area for Bangkok (no prefix). | | ### districts.json, each row One district: an amphoe, or a khet of Bangkok. | field | type | description | example | |---|---|---|---| | `code` | string | DOPA district code: 4 digits, the province code plus 2. | `"1001"` | | `province_code` | string | DOPA province code: 2 digits, as a string. | `"10"` | | `name` | object | Names by BCP 47 language tag, without the administrative prefix (see kind for the prefix table). Consumers must ignore language tags they do not know; more may be added in v1. | | | `kind` | one of `amphoe`, `khet` | amphoe (Thai prefix อำเภอ, English 'District'), or khet in Bangkok (Thai prefix เขต, English 'Khet'). | | ### subdistricts.json, each row One subdistrict: a tambon, or a khwaeng of Bangkok. | field | type | description | example | |---|---|---|---| | `code` | string | DOPA subdistrict code: 6 digits, the district code plus 2. | `"100101"` | | `district_code` | string | DOPA district code: 4 digits, the province code plus 2. | `"1001"` | | `province_code` | string | Deliberately denormalised from the code, and immutable: an area that changes province gets a new code. | `"10"` | | `name` | object | Names by BCP 47 language tag, without the administrative prefix (see kind for the prefix table). Consumers must ignore language tags they do not know; more may be added in v1. | | | `kind` | one of `tambon`, `khwaeng` | tambon (Thai prefix ตำบล, English 'Subdistrict'), or khwaeng in Bangkok (Thai prefix แขวง, English 'Khwaeng'). | | ### History fields (every area) | field | type | description | example | |---|---|---|---| | `status` | one of `active`, `abolished` | active: in use. abolished: no longer in use; the code is never given to another area. | | | `effective_from` | date or null | When the area came into being; null means unknown (DOPA does not publish it in bulk), not 'since the beginning'. | | | `abolished_on` | date or null | When the area was abolished, from DOPA's cancellation date; null for an active area. | | | `replaced_by` | array or null | Codes, at the same level, of the areas that replaced this one. [] for an active area: nothing has replaced it. For an abolished area: the list of its successors; [] when it is known that nothing replaced it; null when the successors are unknown. DOPA does not publish successors, so every abolished area is null today. | `[]` | ### name (every area) Names by BCP 47 language tag, without the administrative prefix (see kind for the prefix table). Consumers must ignore language tags they do not know; more may be added in v1. | field | type | description | example | |---|---|---|---| | `th` | string | Thai name as DOPA publishes it, prefix removed. Abolished areas keep DOPA's text, which can include abbreviations such as 'อ.' and 'จ.'. | `"พระนคร"` | | `en` | string or null | English name in RTGS romanisation, as DOPA publishes it; null when DOPA has none. Do not transliterate one yourself. | `"Phra Nakhon"` | ### postal_codes.json, each row One postal code of one subdistrict. The relation is many-to-many: a subdistrict can have several codes and a code serves many subdistricts. | field | type | description | example | |---|---|---|---| | `postal_code` | string | Thai postal code: 5 digits, as a string. Its first two digits are a postal area, which is not the DOPA province code (Samut Prakan, DOPA 11, uses 10xxx). Never derive a postal code from a district or province code. | `"10200"` | | `subdistrict_code` | string | DOPA subdistrict code: 6 digits, the district code plus 2. | `"100101"` | | `sources` | array | Which sources list this code for this subdistrict. | `["thailandpost","mdes"]` | | `kind` | one of `standard`, `facility` | standard: a delivery postal code. facility: the code of a mail-processing facility (e.g. 10001, Suvarnabhumi Mail Center), not of the places around it. | | | `coverage` | one of `full`, `partial` | full: the subdistrict's only standard postal code. partial: one of several, so it may serve only part of the subdistrict (some tambon are split by village), or a facility code. Do not assume a partial code is valid for every address in the subdistrict. | | | `note` | object or null | A localized note, keyed by BCP 47 language like name: for a facility code the facility's name, for a verified cross-province code the evidence. null for an ordinary link, and null per language that no source gives (nothing is translated). Consumers must ignore languages they do not know. | `null` | | `status` | one of `active`, `abolished` | active: the sources list this code for this subdistrict. abolished: a published snapshot had the link and the sources no longer give it. Links are never removed from this file, so filter on status: 'active' to get the codes in use. | | | `effective_from` | date or null | When the code started to serve this subdistrict; null means unknown. | | | `abolished_on` | date or null | For an abolished link, the date of the snapshot whose sources first no longer gave it (the change happened on or before it); null when active. | | ### changes.json, each row One change against the previous published snapshot (manifest.previous_snapshot). To catch up over several snapshots, rebuild from the history fields of the latest one rather than chaining changes files. There is no 'moved': a move is an abolished code with replaced_by plus an added code. | field | type | description | example | |---|---|---|---| | `level` | one of `province`, `district`, `subdistrict` | | | | `code` | string | | `"100101"` | | `change` | one of `added`, `renamed`, `abolished`, `postal_codes_changed`, `replaced_by_changed` | The kind of change. More kinds may be added within v1: ignore change kinds you do not know. | | | `before` | see description | Shape fixed per change kind; see the if/then rules. | | | `after` | see description | Shape fixed per change kind; see the if/then rules. | | | `effective_on` | date or null | When the change took effect, when known (the abolition date for abolished); null otherwise. | | ### index/postal_codes.json, each row One postal code and every subdistrict it is linked to. | field | type | description | example | |---|---|---|---| | `postal_code` | string | Thai postal code: 5 digits, as a string. Its first two digits are a postal area, which is not the DOPA province code (Samut Prakan, DOPA 11, uses 10xxx). Never derive a postal code from a district or province code. | `"10200"` | | `subdistricts` | array | The subdistricts linked to this postal code, by code, with the link's kind, coverage and status (as in postal_codes.json). | `[{"code":"100101","kind":"standard","coverage":"full","status":"active"}]` | ### by-province/{code}.json by-province/.json: one province with its districts, subdistricts and their postal links, abolished ones included, derived from the main files. For a frontend that needs one province at a time. Unlike the other data files, data is an object. | field | type | description | example | |---|---|---|---| | `schema_version` | string | Semantic version of this schema. A minor version only adds fields or files; renaming, removing or changing the meaning of anything is a new path (v2). | `"1.0.0"` | | `snapshot` | string | Snapshot id: the UTC publish date, with .1, .2 ... for a same-day republish. Pin this id; do not build on latest.json. | `"2026-10-14"` | | `data` | object | | | ### manifest.json manifest.json: what the snapshot contains and where it came from. | field | type | description | example | |---|---|---|---| | `schema_version` | string | Semantic version of this schema. A minor version only adds fields or files; renaming, removing or changing the meaning of anything is a new path (v2). | `"1.0.0"` | | `snapshot` | string | Snapshot id: the UTC publish date, with .1, .2 ... for a same-day republish. Pin this id; do not build on latest.json. | `"2026-10-14"` | | `generated_at` | string | When the snapshot was built (UTC, ISO 8601). | `"2026-10-14T16:00:00Z"` | | `previous_snapshot` | snapshot_id or null | The published snapshot changes.json is against; null for the first one. | | | `counts` | object | Row counts per file and level. | | | `files` | object | Every file of the snapshot except the manifest itself, with its sha256 and size. | | | `sources` | array | The official sources this snapshot was built from. | | ### v1/latest.json v1/latest.json: the newest published snapshot, for humans and discovery. Short cache. Pin a snapshot id in code instead of reading this. | field | type | description | example | |---|---|---|---| | `schema_version` | string | Semantic version of this schema. A minor version only adds fields or files; renaming, removing or changing the meaning of anything is a new path (v2). | `"1.0.0"` | | `snapshot` | string | Snapshot id: the UTC publish date, with .1, .2 ... for a same-day republish. Pin this id; do not build on latest.json. | `"2026-10-14"` | | `manifest_sha256` | string | | | ### v1/snapshots.json v1/snapshots.json: every snapshot ever uploaded. A withdrawn one stays reachable at its immutable URL but must not be used. | field | type | description | example | |---|---|---|---| | `schema_version` | string | Semantic version of this schema. A minor version only adds fields or files; renaming, removing or changing the meaning of anything is a new path (v2). | `"1.0.0"` | | `snapshots` | array | | |