Open Source · 2026
Thai Zip
Type part of a Thai address and get the full subdistrict, district, province, and postal code. A search library on npm, plus a CLI that adds ready-made React components to your project.
The problem
If you've built a Thai checkout form, you know the address part. Subdistrict, district, province, postal code — four fields that people get wrong all the time.
Working in e-commerce, I built that autocomplete again and again. The search logic was the same every time. Only the look changed from project to project.
So I built the search once, properly, and made the UI easy to restyle.
What I built
Thai Zip is two npm packages that work together.
The address data is included: 77 provinces, 928 districts, 7,436 subdistricts, and 965 postal codes. It comes from the open-source kongvut/thai-province-data project (MIT license).
- provinces
- 77
- districts
- 928
- subdistricts
- 7,436
- postal codes
- 965
Technical decisions
How a search works
Here's what happens when someone types “ต.ลาดพร้าว” (Lat Phrao subdistrict):
- 01Clean up the text“ต.ลาดพร้าว” becomes “ลาดพราว”
- 02Cut it into 3-letter piecesลาด · าดพ · ดพร · พรา · ราว
- 03Score matching addresses% of the pieces each address contains
- 04Sorthighest score first; ties go to the closest name
- 05Show suggestionsลาดพร้าว › เขตลาดพร้าว · 10230
Clean up the text. Words like จังหวัด, อำเภอ, ตำบล, แขวง, เขต and short forms like จ. อ. ต. are removed, and so are tone marks. That's why “ต.ลาดพร้าว” and “ลาดพราว” (no tone mark) find the same place.
Cut it into pieces. The text is cut into overlapping 3-letter pieces, called trigrams. Each address gets a score: the share of the typed pieces it contains. It needs at least 40% to show up. Because the score comes from pieces, a slightly incomplete query still works — “ลาดพร้า” (last letter missing) still finds ลาดพร้าว first.
Sort. Higher scores come first. If two addresses tie, the one whose own subdistrict name matches what you typed wins. Typing “ลาดพร้าว” puts the subdistrict ลาดพร้าว above จรเข้บัว, which is only in the same district. Anything still tied is sorted in Thai alphabetical order.
English and postal codes
Loading only what's needed
loadDefaultIndex(), and building the search index from it takes about 30 ms, once.thaizip is the search, thaizip/react is a React hook, and thaizip/data is the address data. Each is a separate import, so a project that brings its own address data never downloads the built-in one.Keeping the data out of the first page load wasn't automatic. Until v0.7.5, the build tool bundled the data into the same file as the loader. Measured in a real Next.js app, adding one dropdown made the first page load 176.7 KB (gzip) heavier — even if the user never touched it. Splitting the data into its own file fixed that: the loader alone is now 2.3 KB.
Another measured fix: Thai names were sorted with localeCompare("th"), which sets up Thai sorting rules again on every comparison. Creating one Intl.Collator and reusing it made sorting 15–27× faster.
Outcome
Real searches against the current version:
ลาดพร้าวลาดพร้าว › เขตลาดพร้าว › กรุงเทพมหานคร · 10230Thailadpraoลาดพร้าว › เขตลาดพร้าว › กรุงเทพมหานคร · 10230common English spellingต.ลาดพร้าวลาดพร้าว › เขตลาดพร้าว › กรุงเทพมหานคร · 10230with a prefix10230all 5 subdistricts that use this postal codepostal code84320บ่อผุด › เกาะสมุย › สุราษฎร์ธานี · 84320island data, fixed in v0.7.8
To add the React components to a project:
npm i thaizip
npx react-thaizip init
npx react-thaizip add autocomplete
init checks your project — package manager, TypeScript, Tailwind, and shadcn/ui (Base UI, Radix, or React Aria) — and installs thaizip. add copies a component in. There are five: search box, step-by-step dropdowns, full address form, read-only address display, and a react-hook-form field.
New versions are released automatically from commit messages (release-please). npm publishing uses trusted publishing, so no npm token is stored in the repo. The library has 339 automated tests, including one that fails if search results change order unexpectedly. The live demo on this page runs on thaizip 0.7.8.
What I learned
The biggest takeaway: keep the search logic small and separate from the UI, and give people the UI as code they own. Then one well-tested library can power every checkout form, whatever it looks like.