Skip to content

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 geothai
import { 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 geothai
from 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/maps
import { 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); // 50

Python

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))  # 12

Spreadsheets

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.