Docs
GeoThai is open data for 77 provinces, 928 districts, 7,436 subdistricts and 976 postal codes. Every file below is in the GeoThai/data repository, release v2026.10.22.
- json/provinces.jsonProvinces77
- json/districts.jsonDistricts, with their province928
- json/subdistricts.jsonSubdistricts, with their district, province and postal codes7,436
- json/tree.jsonProvince > district > subdistrict in one nested file7,436
- json/abolished.jsonAreas that no longer exist, with the date they ended and the codes that took their place1,583
- csv/*.csvThe same tables as CSV, UTF-8 with a BOM
- datapackage.jsonField types, sources, license and row counts
Formats
The main files hold active areas only, sorted by code. Codes are strings, so a leading zero never drops. Codes nest: 10 is a province, 1001 a district in it, 100101 a subdistrict in that district.
provinces
- codestringDOPA province code
- name_thstringThai name
- name_enstringEnglish name
districts
- codestringDOPA district code
- name_thstringThai name
- name_enstringEnglish name
- province_codestringParent province
subdistricts
- codestringDOPA subdistrict code
- name_thstringThai name
- name_enstringEnglish name
- district_codestringParent district
- province_codestringParent province
- postal_codesarrayThailand Post codes. More than one when the subdistrict spans post office zones
abolished
- codestringDOPA code
- name_thstringThai name
- name_enstringEnglish name. Null in JSON and empty in CSV when DOPA gives none
- levelstringArea level
- parent_codestringParent area. Null in JSON and empty in CSV for a province
- abolished_ondateDate the area was abolished
- successorsarrayAreas that took its place. Empty when GeoThai has no notice for the change and no clear name match
- successors_basisstringnotice: from a reviewed notice, see successors_source. name: the one active subdistrict with the same Thai name in the province, for codes DOPA dropped in its 2006 cleanup; likely but not proven
- successors_in_forcedateDate the change took legal effect. DOPA often updates its list later, so it can be earlier than abolished_on
- successors_sourcestringThe notice that made the change: title, Royal Gazette volume, part, page and URL
subdistricts, one record
{ "code": "100101", "name_th": "พระบรมมหาราชวัง", "name_en": "Phra Borom Maha Ratchawang", "district_code": "1001", "province_code": "10", "postal_codes": ["10200"]}In CSV, postal_codes is one cell with the codes separated by a space.
tree.json
[
{
"code": "10",
"name_th": "กรุงเทพมหานคร",
"name_en": "Bangkok",
"districts": [
{
"code": "1001",
"name_th": "เขตพระนคร",
"name_en": "Phra Nakhon",
"subdistricts": [
{
"code": "100101",
"name_th": "พระบรมมหาราชวัง",
"name_en": "Phra Borom Maha Ratchawang",
"postal_codes": ["10200"]
}
]
}
]
}
]Built for address forms: pick a province, then a district, then a subdistrict, from one file.
Packages
The same data as a package for JavaScript and Python, with types, lookups by code and postal code, search by Thai or English name, and a match for hand-written addresses. Each data release ships as a patch version.
JavaScript
npm install geothaipnpm add geothaiyarn add geothaibun add geothaiimport { address, match, search } from 'geothai';
address('840503')?.subdistrict.name_en; // 'Ko Tao'
search('phangan').map((r) => r.level); // ['district', 'subdistrict']
match({ subdistrict: 'ต.ตลาด', district: 'อ.เมือง', province: 'สุราษฎร์ธานี' })[0]?.subdistrict.code;
// '840101'Python
pip install geothaiuv add geothaipoetry add geothaifrom geothai import address, match, search
address("840503").subdistrict.name_en # 'Ko Tao'
[r.level for r in search("phangan")] # ['district', 'subdistrict']
match("ต.ตลาด", district="อ.เมือง", province="สุราษฎร์ธานี")[0].subdistrict.code
# '840101'Maps
Province, district and subdistrict maps on the same codes, as SVG. The shapes come from GeoThai/boundaries, already projected, so a map needs no map library and renders on the server. Names in any form become codes with fromNames().
npm install @geothai/mapspnpm add @geothai/mapsyarn add @geothai/mapsbun add @geothai/mapsimport { choropleth, fromNames, provinces, render, toSvg } from '@geothai/maps';
const { found } = fromNames(['จ.เชียงใหม่', 'Phuket'], provinces);
const { fills } = choropleth(provinces, { [found['จ.เชียงใหม่']]: 381, [found.Phuket]: 150 });
const svg = toSvg(render(provinces, { fills, title: 'Thailand' }));The same map as a component, with the same markup:
Usage
To read the files without a package, pin a release in the URL, so the data does not change under you. jsDelivr serves every file of every release.
JavaScript
const base = 'https://cdn.jsdelivr.net/gh/GeoThai/data@v2026.10.22';
const tree = await (await fetch(`${base}/json/tree.json`)).json();
const bangkok = tree.find((province) => province.code === '10');
console.log(bangkok.districts.length); // 50Python
import json, urllib.request
url = "https://cdn.jsdelivr.net/gh/GeoThai/data@v2026.10.22/json/subdistricts.json"
subdistricts = json.load(urllib.request.urlopen(url))
same_code = [s for s in subdistricts if "10200" in s["postal_codes"]]
print(len(same_code)) # 12Spreadsheets
Download the CSV files and open them in Excel, Google Sheets or Numbers. Each file starts with a UTF-8 byte order mark, so Excel shows the Thai names without any import settings.
Versions
A version is the release date in Thailand, such as v2026.10.22.
- A weekly rebuild reads every source again. It makes a release only if an area, a name or a postal code changed.
- A release never changes after it ships. Point your code at a version, not at the main branch.
- Each release lists every area that changed and why in the changelog.
- Areas that are abolished leave the main files and move to abolished.json, with the date they ended.
- The old data, v1 to v4, has a different format. It stays on the archive branch and the v1 to v4 tags.