thai_provinces
ข้อมูลเขตการปกครองของประเทศไทย (จังหวัด/อำเภอ/ตำบล พร้อมรหัสไปรษณีย์) สำหรับภาษา Dart ฝังข้อมูลมาในตัว ค้นหา/เติมคำอัตโนมัติ/ตรวจสอบที่อยู่ได้ทันที ไม่ต้องต่อเน็ต ไม่พึ่ง Flutter
A pure-Dart, MIT-licensed library of Thailand's administrative areas — 77 provinces (จังหวัด), 928 districts (อำเภอ/เขต), and 7,452 subdistricts (ตำบล/แขวง) with postal codes — fully embedded, with lookup, hierarchy navigation, autocomplete, validation, and address resolution. No network or files at runtime, and no Flutter dependency (works in CLI, server and Flutter alike).
Install
dependencies:
thai_provinces: ^0.2.0
dart pub add thai_provinces
Requires Dart SDK 3.0+.
Features
- Embedded dataset — zero network/filesystem access at runtime; parsed once, lazily, on first use.
- Complete coverage — 77 provinces, 928 districts, 7,452 subdistricts, postcodes.
- Official DOPA geocodes — 2-digit provinces (10–96), 4-digit districts,
6-digit subdistricts; the hierarchy is derivable
(
districtCode ~/ 100 == provinceCode,subCode ~/ 100 == districtCode). - Lookups by code and hierarchy navigation (parent/child both ways).
- Postal-code lookups — subdistricts by postcode, or postcodes of a district.
- Name search & autocomplete — exact (normalized) find plus prefix search.
- Validation of a province/district/subdistrict triple.
- Address resolution — match free-form fragments (plus an optional postcode)
to concrete
Province/District/Subdistrictresults. - Cascading address forms — drive province → district → subdistrict dropdowns and auto-fill the postcode (see the recipe below).
- JSON serialization — every model has
toJson()/fromJsonwith a round-trip guarantee, ready for REST APIs and local storage. - Thai + English names for every area, plus six regions.
Quick start
Build a cascading address form
The most common use: three linked dropdowns — province → district → subdistrict
— that auto-fill the postcode. The data layer is identical whether you wire it to
Flutter DropdownButtonFormFields or a CLI prompt.
import 'package:thai_provinces/thai_provinces.dart';
// 1. Province dropdown — all 77, ordered by code:
final provinceItems = provinces();
// 2. User picks a province -> show its districts:
final selectedProvince = provinceByCode(50)!; // เชียงใหม่
final districtItems = selectedProvince.districts; // ordered by code
// 3. User picks a district -> show its subdistricts:
final selectedDistrict = districtItems.first;
final subdistrictItems = selectedDistrict.subdistricts;
// 4. User picks a subdistrict -> the postcode auto-fills:
final selectedSubdistrict = subdistrictItems.first;
final postcode = selectedSubdistrict.postcode; // the official postcode
print('${selectedProvince.nameTh} > ${selectedDistrict.nameTh} > '
'${selectedSubdistrict.nameTh} — $postcode');
In Flutter, feed each list to a DropdownButtonFormField, and clear the child
selections whenever a parent changes. Everything is in memory, so rebuilding the
options on every change is instant.
Look up by code and read names
import 'package:thai_provinces/thai_provinces.dart';
final p = provinceByCode(10); // nullable; null if unknown
print('${p!.nameTh} / ${p.nameEn}'); // กรุงเทพมหานคร / Bangkok
print(p.region.nameEn); // Central
Navigate the hierarchy
final cm = provinceByCode(50); // เชียงใหม่ / Chiang Mai
for (final d in cm!.districts) {
print('${d.code} ${d.nameTh}');
for (final s in d.subdistricts) {
print(' ${s.code} ${s.nameTh} (${s.postcode})');
}
}
// Walk back up from a subdistrict (nullable parents):
final s = subdistrictByCode(100101);
print('${s!.nameTh} -> ${s.district?.nameTh} -> ${s.province?.nameTh}');
Find by postal code
for (final s in byPostcode(50200)) {
print('${s.nameTh}, ${s.district?.nameTh}, ${s.province?.nameTh}');
}
// All postcodes within a district:
print(postcodesOf(5001)); // e.g. [50000, 50100, 50200, 50300]
Autocomplete (prefix search)
for (final s in searchSubdistricts('สุเทพ')) {
print('${s.nameTh} — ${s.district?.nameTh}, ${s.province?.nameTh}');
}
Resolve a full address
try {
final matches = resolve(const AddressQuery(
subdistrict: 'สุเทพ',
district: 'เมืองเชียงใหม่',
province: 'เชียงใหม่',
postcode: 50200,
));
for (final m in matches) {
print('${m.province.nameTh} > ${m.district.nameTh} > '
'${m.subdistrict.nameTh} (${m.subdistrict.postcode})');
}
} on ThaiAddressException catch (e) {
// no match / ambiguous / no usable field
print(e.message);
}
Serialize to / from JSON
Every model (Province, District, Subdistrict, AddressMatch) is
self-describing: toJson() emits all fields, and fromJson rebuilds the value
with no dataset lookup, so X.fromJson(x.toJson()) == x. A Province's
region is stored as its integer region code (1..6).
import 'dart:convert';
import 'package:thai_provinces/thai_provinces.dart';
final p = provinceByCode(10)!; // Bangkok
final wire = jsonEncode(p.toJson());
// {"code":10,"nameTh":"กรุงเทพมหานคร","nameEn":"Bangkok","region":2}
final back = Province.fromJson(jsonDecode(wire) as Map<String, dynamic>);
print(back == p); // true
// AddressMatch nests each level's JSON:
final m = resolve(const AddressQuery(subdistrict: 'สุเทพ', postcode: 50200)).first;
final m2 = AddressMatch.fromJson(
jsonDecode(jsonEncode(m.toJson())) as Map<String, dynamic>);
print(m2 == m); // true
An unknown region code passed to Province.fromJson throws a
FormatException; a missing/wrongly-typed key throws.
Validate a code triple
try {
validate(50, 5001, 500101);
} on ThaiAddressException catch (e) {
print('invalid address codes: ${e.message} (${e.kind})');
}
Caveat: duplicate names
Many subdistrict names repeat across different provinces (e.g. "ในเมือง" names
22 subdistricts across 19 provinces), so name-based lookups (findSubdistricts,
searchSubdistricts) return a List, not a single result. Disambiguate
with district/province context or a postcode — resolve(AddressQuery(...)) is
built for exactly this.
Note also that Bangkok uses เขต/แขวง terminology (its district names carry the "เขต" sense), and that postal codes attach at the subdistrict level — a single district can span several postcodes.
Notes on normalization (NFC)
normalizeName trims and collapses whitespace, lowercases (ASCII/Latin only;
Thai has no case), and strips one leading admin prefix
(จังหวัด/อำเภอ/อ./ตำบล/ต./เขต/แขวง/กิ่งอำเภอ and English changwat/amphoe/khet/
tambon/khwaeng). Unlike the Go original it does not apply Unicode NFC: the
Dart core library ships no NFC implementation, and the embedded dataset is
already NFC, so composed/decomposed folding is unnecessary in practice.
Data source & license
This library is MIT-licensed (see LICENSE).
The embedded dataset is a reshaped snapshot of
github.com/kongvut/thai-province-data
by Kongvut Sangkla, used under the MIT License, with district codes validated
against the Department of Provincial Administration (กรมการปกครอง, DOPA)
dataset published on Thailand's open-government-data portal
(data.go.th).
Official two-digit province codes were derived and the field layout slimmed, but
it is the same underlying data. The factual codes/names are government
administrative data; credit DOPA when redistributing.
This package is a faithful pure-Dart port of the Go library
go-thaiaddress.
Libraries
- thai_provinces
- Thailand's administrative-area data — every province (จังหวัด), district (อำเภอ/เขต) and subdistrict (ตำบล/แขวง) together with its postal code — plus lookup, hierarchy navigation, name search, autocomplete and validation, all served from data embedded in the package (no network, no files at runtime).