← All projects

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.

TypeScriptnpmReact
Loading Thai address demo

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.

thaizipThe search library. Search in Thai or English, look up by postal code, or list provinces → districts → subdistricts for step-by-step dropdowns. It has no other dependencies and works in the browser or on a server.
react-thaizipA command-line tool for React and Next.js. It copies ready-made address components into your project as normal source files, so you can change how they look.

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):

  1. 01Clean up the text“ต.ลาดพร้าว” becomes “ลาดพราว”
  2. 02Cut it into 3-letter piecesลาด · าดพ · ดพร · พรา · ราว
  3. 03Score matching addresses% of the pieces each address contains
  4. 04Sorthighest score first; ties go to the closest name
  5. 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

English spellingsThe data uses official spellings like “Lat Phrao”, but people type “ladprao” or “lardprao”. A list of common spellings maps them to the official name, so they find the right place.
Postal codesTyping only numbers searches by postal code. A full 5-digit code shows every subdistrict that uses it — 230 postal codes are shared by more than 10 subdistricts, so the results aren't cut short.

Loading only what's needed

Data loads only when neededThe address data is about 120 KB (gzip) and lives in its own file. It downloads only when your code calls loadDefaultIndex(), and building the search index from it takes about 30 ms, once.
Import only what you usethaizip 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:

  • ลาดพร้าวลาดพร้าว › เขตลาดพร้าว › กรุงเทพมหานคร · 10230Thai
  • ladpraoลาดพร้าว › เขตลาดพร้าว › กรุงเทพมหานคร · 10230common English spelling
  • ต.ลาดพร้าวลาดพร้าว › เขตลาดพร้าว › กรุงเทพมหานคร · 10230with a prefix
  • 10230all 5 subdistricts that use this postal codepostal code
  • 84320บ่อผุด › เกาะสมุย › สุราษฎร์ธานี · 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

Check the data, not just the code9 districts and 24 subdistricts whose names start with “เกาะ” (island) were missing from the data, so postal codes like 84320 (Ko Samui) found nothing. v0.7.8 restored them, and the data files are now locked with checksums: if they change by accident, the build fails.
Measure before optimizingThe two biggest wins — data bundled into the first page load, and slow Thai sorting — were found by measuring, not guessing.

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.