thaizip 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,6 +40,9 @@ const results2 = searchThaiAddress(index, '10900')
40
40
  // ค้นหาด้วยภาษาอังกฤษ
41
41
  const results3 = searchThaiAddress(index, 'chiang mai')
42
42
 
43
+ // ⚠️ ไม่รองรับ: ค้นหาแบบรวม text + รหัสไปรษณีย์ในครั้งเดียว (เช่น "ลาดพร้าว 10900")
44
+ // ให้ค้นหาด้วยชื่อสถานที่ หรือรหัสไปรษณีย์ แยกกัน
45
+
43
46
  console.log(results)
44
47
  // [{ tambonId, tambonNameTh, tambonNameEn, amphureId, amphureNameTh, ... zipCode }, ...]
45
48
  ```
@@ -102,16 +105,22 @@ import { loadDefaultIndex } from 'thaizip/data'
102
105
  import { useThaiAddressAutocomplete } from 'thaizip'
103
106
  import type { TrigramIndex } from 'thaizip'
104
107
 
105
- function AddressForm() {
108
+ // แนะนำ: แยก loading และ form ออกจากกัน เพื่อให้ hook เรียกหลัง index พร้อมแล้วเสมอ
109
+ function AddressPage() {
106
110
  const [index, setIndex] = useState<TrigramIndex | null>(null)
107
111
 
108
112
  useEffect(() => {
109
113
  loadDefaultIndex().then(setIndex)
110
114
  }, [])
111
115
 
116
+ if (!index) return <p>กำลังโหลด...</p>
117
+ return <AddressForm index={index} />
118
+ }
119
+
120
+ function AddressForm({ index }: { index: TrigramIndex }) {
112
121
  const { query, setQuery, suggestions, isOpen, selectSuggestion, clear } =
113
122
  useThaiAddressAutocomplete({
114
- index: index!,
123
+ index,
115
124
  limit: 10, // default: 10
116
125
  debounce: 200, // default: 200ms
117
126
  threshold: 0.4, // default: 0.4
@@ -119,8 +128,6 @@ function AddressForm() {
119
128
 
120
129
  const [address, setAddress] = useState(null)
121
130
 
122
- if (!index) return <p>กำลังโหลด...</p>
123
-
124
131
  return (
125
132
  <div>
126
133
  <input
@@ -207,10 +214,10 @@ GET /address/search?q=10900
207
214
  import { buildThaiAddressIndex } from 'thaizip'
208
215
 
209
216
  const index = buildThaiAddressIndex({
210
- geographies: [...],
211
217
  provinces: [...],
212
218
  amphures: [...],
213
219
  tambons: [...],
220
+ // geographies ไม่จำเป็นต้องระบุ (ไม่ได้ใช้งานโดย indexer)
214
221
  })
215
222
  ```
216
223
 
@@ -251,21 +258,29 @@ type ResolvedThaiAddress = {
251
258
 
252
259
  ---
253
260
 
254
- ## Migration จาก v0.x
261
+ ## Migration จาก v0.2.x → v0.3.0+
255
262
 
256
263
  ```ts
257
- // v0.x (sync)
264
+ // v0.2.x (sync, bundle ~630KB)
258
265
  import { defaultIndex } from 'thaizip/data'
259
266
  const results = searchThaiAddress(defaultIndex, query)
260
267
 
261
- // v1.0 (async)
268
+ // v0.3.0+ (async, bundle ~132KB gzip)
262
269
  import { loadDefaultIndex } from 'thaizip/data'
263
- const index = await loadDefaultIndex() // cache อัตโนมัติ
270
+ const index = await loadDefaultIndex() // cache อัตโนมัติ หลังจากโหลดครั้งแรก
264
271
  const results = searchThaiAddress(index, query)
265
272
  ```
266
273
 
267
274
  ---
268
275
 
276
+ ## ข้อมูลที่อยู่
277
+
278
+ ข้อมูลอ้างอิงจากการแบ่งเขตการปกครองของไทย (77 จังหวัด, 920 อำเภอ, ~7,385 ตำบล)
279
+
280
+ ตำบลที่ยังมีสถานะ active แต่อ้างอิงอำเภอที่ถูก soft-deleted จะถูกข้ามโดย index โดยอัตโนมัติ พร้อมแสดง `console.warn` ให้ทราบ
281
+
282
+ ---
283
+
269
284
  ## License
270
285
 
271
286
  MIT
package/dist/data.cjs CHANGED
@@ -70,7 +70,12 @@ function buildThaiAddressIndex(data) {
70
70
  for (const tambon of tambons) {
71
71
  if (tambon.deleted_at) continue;
72
72
  const amphure = ampMap.get(tambon.amphure_id);
73
- if (!amphure) continue;
73
+ if (!amphure) {
74
+ console.warn(
75
+ `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): amphure_id=${tambon.amphure_id} not found or deleted`
76
+ );
77
+ continue;
78
+ }
74
79
  const province = provMap.get(amphure.province_id);
75
80
  if (!province) continue;
76
81
  const record = {
@@ -115,6 +120,9 @@ function buildThaiAddressIndex(data) {
115
120
 
116
121
  // src/data/loader.ts
117
122
  var cached = null;
123
+ function clearDefaultIndex() {
124
+ cached = null;
125
+ }
118
126
  async function loadDefaultIndex() {
119
127
  if (cached) return cached;
120
128
  const { p: p2, a: a2, t: t2 } = await Promise.resolve().then(() => (init_defaultData(), defaultData_exports));
@@ -126,4 +134,5 @@ async function loadDefaultIndex() {
126
134
  return cached;
127
135
  }
128
136
 
137
+ exports.clearDefaultIndex = clearDefaultIndex;
129
138
  exports.loadDefaultIndex = loadDefaultIndex;
package/dist/data.d.cts CHANGED
@@ -16,6 +16,7 @@ type TrigramIndex = {
16
16
  zipIndex: Map<string, number[]>;
17
17
  };
18
18
 
19
+ declare function clearDefaultIndex(): void;
19
20
  declare function loadDefaultIndex(): Promise<TrigramIndex>;
20
21
 
21
- export { loadDefaultIndex };
22
+ export { clearDefaultIndex, loadDefaultIndex };
package/dist/data.d.ts CHANGED
@@ -16,6 +16,7 @@ type TrigramIndex = {
16
16
  zipIndex: Map<string, number[]>;
17
17
  };
18
18
 
19
+ declare function clearDefaultIndex(): void;
19
20
  declare function loadDefaultIndex(): Promise<TrigramIndex>;
20
21
 
21
- export { loadDefaultIndex };
22
+ export { clearDefaultIndex, loadDefaultIndex };
package/dist/data.js CHANGED
@@ -68,7 +68,12 @@ function buildThaiAddressIndex(data) {
68
68
  for (const tambon of tambons) {
69
69
  if (tambon.deleted_at) continue;
70
70
  const amphure = ampMap.get(tambon.amphure_id);
71
- if (!amphure) continue;
71
+ if (!amphure) {
72
+ console.warn(
73
+ `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): amphure_id=${tambon.amphure_id} not found or deleted`
74
+ );
75
+ continue;
76
+ }
72
77
  const province = provMap.get(amphure.province_id);
73
78
  if (!province) continue;
74
79
  const record = {
@@ -113,6 +118,9 @@ function buildThaiAddressIndex(data) {
113
118
 
114
119
  // src/data/loader.ts
115
120
  var cached = null;
121
+ function clearDefaultIndex() {
122
+ cached = null;
123
+ }
116
124
  async function loadDefaultIndex() {
117
125
  if (cached) return cached;
118
126
  const { p: p2, a: a2, t: t2 } = await Promise.resolve().then(() => (init_defaultData(), defaultData_exports));
@@ -124,4 +132,4 @@ async function loadDefaultIndex() {
124
132
  return cached;
125
133
  }
126
134
 
127
- export { loadDefaultIndex };
135
+ export { clearDefaultIndex, loadDefaultIndex };
package/dist/index.cjs CHANGED
@@ -46,7 +46,12 @@ function buildThaiAddressIndex(data) {
46
46
  for (const tambon of tambons) {
47
47
  if (tambon.deleted_at) continue;
48
48
  const amphure = ampMap.get(tambon.amphure_id);
49
- if (!amphure) continue;
49
+ if (!amphure) {
50
+ console.warn(
51
+ `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): amphure_id=${tambon.amphure_id} not found or deleted`
52
+ );
53
+ continue;
54
+ }
50
55
  const province = provMap.get(amphure.province_id);
51
56
  if (!province) continue;
52
57
  const record = {
@@ -88,15 +93,18 @@ function buildThaiAddressIndex(data) {
88
93
  }
89
94
  return { map, records, zipIndex };
90
95
  }
96
+ function clearDefaultIndex() {
97
+ }
91
98
 
92
99
  // src/core/search.ts
93
100
  var ZIP_CODE_RE = /^\d+$/;
94
101
  function searchThaiAddress(index, query, options) {
95
102
  const limit = options?.limit ?? 10;
96
103
  const threshold = options?.threshold ?? 0.4;
97
- if (!query) return [];
104
+ if (!index || !query) return [];
98
105
  const normalized = normalizeThaiAddressText(query);
99
106
  if (normalized.length === 0) return [];
107
+ if (normalized.length > 300) return [];
100
108
  if (ZIP_CODE_RE.test(normalized)) {
101
109
  if (normalized.length < 2) return [];
102
110
  const matches = [];
@@ -113,7 +121,6 @@ function searchThaiAddress(index, query, options) {
113
121
  return matches.slice(0, limit);
114
122
  }
115
123
  const queryTrigrams = extractTrigramsNormalized(normalized);
116
- if (queryTrigrams.size === 0) return [];
117
124
  const hits = /* @__PURE__ */ new Map();
118
125
  for (const trigram of queryTrigrams) {
119
126
  const candidates = index.map.get(trigram);
@@ -129,7 +136,12 @@ function searchThaiAddress(index, query, options) {
129
136
  scored.push({ idx, score });
130
137
  }
131
138
  }
132
- scored.sort((a, b) => b.score - a.score);
139
+ scored.sort((a, b) => {
140
+ if (b.score !== a.score) return b.score - a.score;
141
+ const ra = index.records[a.idx];
142
+ const rb = index.records[b.idx];
143
+ return ra.provinceNameTh.localeCompare(rb.provinceNameTh) || ra.amphureNameTh.localeCompare(rb.amphureNameTh) || ra.tambonNameTh.localeCompare(rb.tambonNameTh);
144
+ });
133
145
  return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
134
146
  }
135
147
 
@@ -223,6 +235,7 @@ function useThaiAddressAutocomplete(options) {
223
235
  }
224
236
 
225
237
  exports.buildThaiAddressIndex = buildThaiAddressIndex;
238
+ exports.clearDefaultIndex = clearDefaultIndex;
226
239
  exports.formatThaiAddressSuggestion = formatThaiAddressSuggestion;
227
240
  exports.normalizeThaiAddressText = normalizeThaiAddressText;
228
241
  exports.resolveThaiAddress = resolveThaiAddress;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":["useState","useRef","useCallback","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["const TONE_MARKS = /[\\u0E48-\\u0E4B\\u0E47\\u0E4C]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) continue\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n const fields = [\n record.tambonNameTh,\n record.tambonNameEn,\n record.amphureNameTh,\n record.amphureNameEn,\n record.provinceNameTh,\n record.provinceNameEn,\n record.zipCode,\n ]\n\n for (const field of fields) {\n for (const trigram of extractTrigrams(field)) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n }\n }\n\n return { map, records, zipIndex }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode)\n })\n return matches.slice(0, limit)\n }\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n if (queryTrigrams.size === 0) return []\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => b.score - a.score)\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n","// NOTE (SZ-4): CJS consumers (`dist/index.cjs`) cannot tree-shake this hook out of the bundle.\n// If you are using the CJS build in a non-React environment, import only from the core entry\n// or use the ESM build with a bundler that supports tree-shaking.\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { searchThaiAddress } from '../core/search'\nimport { formatThaiAddressSuggestion } from '../core/formatter'\nimport { resolveThaiAddress } from '../core/resolver'\nimport type {\n UseThaiAddressAutocompleteOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n ResolvedThaiAddress,\n} from '../types'\n\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n const [query, setQueryState] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\n\n const setQuery = useCallback((value: string) => {\n setQueryState(value)\n }, [])\n\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(index, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\n\n /**\n * Resolves a suggestion to a full ThaiAddress and clears the suggestions list.\n *\n * Note: `query` is intentionally left unchanged so the input field retains the\n * typed text. Call `clear()` afterwards if you want to reset the input as well.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n throw new Error(`[thaizip] No record found for suggestion id \"${item.id}\". Make sure to use suggestions returned by this hook.`)\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQueryState('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n return {\n query,\n setQuery,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/data/loader.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":["useState","useRef","useCallback","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["const TONE_MARKS = /[\\u0E48-\\u0E4B\\u0E47\\u0E4C]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n console.warn(\n `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): ` +\n `amphure_id=${tambon.amphure_id} not found or deleted`\n )\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n const fields = [\n record.tambonNameTh,\n record.tambonNameEn,\n record.amphureNameTh,\n record.amphureNameEn,\n record.provinceNameTh,\n record.provinceNameEn,\n record.zipCode,\n ]\n\n for (const field of fields) {\n for (const trigram of extractTrigrams(field)) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n }\n }\n\n return { map, records, zipIndex }\n}\n","import { buildThaiAddressIndex } from '../core/indexer'\nimport type { TrigramIndex } from '../types'\nimport type { CompactProvince, CompactAmphure, CompactTambon } from './defaultData'\n\nlet cached: TrigramIndex | null = null\n\nexport function clearDefaultIndex(): void {\n cached = null\n}\n\nexport async function loadDefaultIndex(): Promise<TrigramIndex> {\n if (cached) return cached\n const { p, a, t } = await import('./defaultData') as {\n p: CompactProvince[]\n a: CompactAmphure[]\n t: CompactTambon[]\n }\n cached = buildThaiAddressIndex({\n provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),\n amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),\n tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code: Number(zip_code), deleted_at: null })),\n })\n return cached\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode)\n })\n return matches.slice(0, limit)\n }\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh) ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh) ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh)\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n","// NOTE (SZ-4): CJS consumers (`dist/index.cjs`) cannot tree-shake this hook out of the bundle.\n// If you are using the CJS build in a non-React environment, import only from the core entry\n// or use the ESM build with a bundler that supports tree-shaking.\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { searchThaiAddress } from '../core/search'\nimport { formatThaiAddressSuggestion } from '../core/formatter'\nimport { resolveThaiAddress } from '../core/resolver'\nimport type {\n UseThaiAddressAutocompleteOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n ResolvedThaiAddress,\n} from '../types'\n\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n const [query, setQueryState] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\n\n const setQuery = useCallback((value: string) => {\n setQueryState(value)\n }, [])\n\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(index, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\n\n /**\n * Resolves a suggestion to a full ThaiAddress and clears the suggestions list.\n *\n * Note: `query` is intentionally left unchanged so the input field retains the\n * typed text. Call `clear()` afterwards if you want to reset the input as well.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n throw new Error(`[thaizip] No record found for suggestion id \"${item.id}\". Make sure to use suggestions returned by this hook.`)\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQueryState('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n return {\n query,\n setQuery,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -78,7 +78,7 @@ type RawTambon = {
78
78
  deleted_at: string | null;
79
79
  };
80
80
  type RawData = {
81
- geographies: RawGeography[];
81
+ geographies?: RawGeography[];
82
82
  provinces: RawProvince[];
83
83
  amphures: RawAmphure[];
84
84
  tambons: RawTambon[];
@@ -86,6 +86,8 @@ type RawData = {
86
86
 
87
87
  declare function buildThaiAddressIndex(data: RawData): TrigramIndex;
88
88
 
89
+ declare function clearDefaultIndex(): void;
90
+
89
91
  declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
90
92
 
91
93
  declare function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion;
@@ -103,4 +105,4 @@ declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteO
103
105
  clear: () => void;
104
106
  };
105
107
 
106
- export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
108
+ export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, clearDefaultIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
package/dist/index.d.ts CHANGED
@@ -78,7 +78,7 @@ type RawTambon = {
78
78
  deleted_at: string | null;
79
79
  };
80
80
  type RawData = {
81
- geographies: RawGeography[];
81
+ geographies?: RawGeography[];
82
82
  provinces: RawProvince[];
83
83
  amphures: RawAmphure[];
84
84
  tambons: RawTambon[];
@@ -86,6 +86,8 @@ type RawData = {
86
86
 
87
87
  declare function buildThaiAddressIndex(data: RawData): TrigramIndex;
88
88
 
89
+ declare function clearDefaultIndex(): void;
90
+
89
91
  declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
90
92
 
91
93
  declare function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion;
@@ -103,4 +105,4 @@ declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteO
103
105
  clear: () => void;
104
106
  };
105
107
 
106
- export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
108
+ export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, clearDefaultIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
package/dist/index.js CHANGED
@@ -44,7 +44,12 @@ function buildThaiAddressIndex(data) {
44
44
  for (const tambon of tambons) {
45
45
  if (tambon.deleted_at) continue;
46
46
  const amphure = ampMap.get(tambon.amphure_id);
47
- if (!amphure) continue;
47
+ if (!amphure) {
48
+ console.warn(
49
+ `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): amphure_id=${tambon.amphure_id} not found or deleted`
50
+ );
51
+ continue;
52
+ }
48
53
  const province = provMap.get(amphure.province_id);
49
54
  if (!province) continue;
50
55
  const record = {
@@ -86,15 +91,18 @@ function buildThaiAddressIndex(data) {
86
91
  }
87
92
  return { map, records, zipIndex };
88
93
  }
94
+ function clearDefaultIndex() {
95
+ }
89
96
 
90
97
  // src/core/search.ts
91
98
  var ZIP_CODE_RE = /^\d+$/;
92
99
  function searchThaiAddress(index, query, options) {
93
100
  const limit = options?.limit ?? 10;
94
101
  const threshold = options?.threshold ?? 0.4;
95
- if (!query) return [];
102
+ if (!index || !query) return [];
96
103
  const normalized = normalizeThaiAddressText(query);
97
104
  if (normalized.length === 0) return [];
105
+ if (normalized.length > 300) return [];
98
106
  if (ZIP_CODE_RE.test(normalized)) {
99
107
  if (normalized.length < 2) return [];
100
108
  const matches = [];
@@ -111,7 +119,6 @@ function searchThaiAddress(index, query, options) {
111
119
  return matches.slice(0, limit);
112
120
  }
113
121
  const queryTrigrams = extractTrigramsNormalized(normalized);
114
- if (queryTrigrams.size === 0) return [];
115
122
  const hits = /* @__PURE__ */ new Map();
116
123
  for (const trigram of queryTrigrams) {
117
124
  const candidates = index.map.get(trigram);
@@ -127,7 +134,12 @@ function searchThaiAddress(index, query, options) {
127
134
  scored.push({ idx, score });
128
135
  }
129
136
  }
130
- scored.sort((a, b) => b.score - a.score);
137
+ scored.sort((a, b) => {
138
+ if (b.score !== a.score) return b.score - a.score;
139
+ const ra = index.records[a.idx];
140
+ const rb = index.records[b.idx];
141
+ return ra.provinceNameTh.localeCompare(rb.provinceNameTh) || ra.amphureNameTh.localeCompare(rb.amphureNameTh) || ra.tambonNameTh.localeCompare(rb.tambonNameTh);
142
+ });
131
143
  return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
132
144
  }
133
145
 
@@ -220,6 +232,6 @@ function useThaiAddressAutocomplete(options) {
220
232
  };
221
233
  }
222
234
 
223
- export { buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
235
+ export { buildThaiAddressIndex, clearDefaultIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
224
236
  //# sourceMappingURL=index.js.map
225
237
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["const TONE_MARKS = /[\\u0E48-\\u0E4B\\u0E47\\u0E4C]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) continue\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n const fields = [\n record.tambonNameTh,\n record.tambonNameEn,\n record.amphureNameTh,\n record.amphureNameEn,\n record.provinceNameTh,\n record.provinceNameEn,\n record.zipCode,\n ]\n\n for (const field of fields) {\n for (const trigram of extractTrigrams(field)) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n }\n }\n\n return { map, records, zipIndex }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode)\n })\n return matches.slice(0, limit)\n }\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n if (queryTrigrams.size === 0) return []\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => b.score - a.score)\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n","// NOTE (SZ-4): CJS consumers (`dist/index.cjs`) cannot tree-shake this hook out of the bundle.\n// If you are using the CJS build in a non-React environment, import only from the core entry\n// or use the ESM build with a bundler that supports tree-shaking.\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { searchThaiAddress } from '../core/search'\nimport { formatThaiAddressSuggestion } from '../core/formatter'\nimport { resolveThaiAddress } from '../core/resolver'\nimport type {\n UseThaiAddressAutocompleteOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n ResolvedThaiAddress,\n} from '../types'\n\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n const [query, setQueryState] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\n\n const setQuery = useCallback((value: string) => {\n setQueryState(value)\n }, [])\n\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(index, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\n\n /**\n * Resolves a suggestion to a full ThaiAddress and clears the suggestions list.\n *\n * Note: `query` is intentionally left unchanged so the input field retains the\n * typed text. Call `clear()` afterwards if you want to reset the input as well.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n throw new Error(`[thaizip] No record found for suggestion id \"${item.id}\". Make sure to use suggestions returned by this hook.`)\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQueryState('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n return {\n query,\n setQuery,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/data/loader.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["const TONE_MARKS = /[\\u0E48-\\u0E4B\\u0E47\\u0E4C]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n console.warn(\n `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): ` +\n `amphure_id=${tambon.amphure_id} not found or deleted`\n )\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n const fields = [\n record.tambonNameTh,\n record.tambonNameEn,\n record.amphureNameTh,\n record.amphureNameEn,\n record.provinceNameTh,\n record.provinceNameEn,\n record.zipCode,\n ]\n\n for (const field of fields) {\n for (const trigram of extractTrigrams(field)) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n }\n }\n\n return { map, records, zipIndex }\n}\n","import { buildThaiAddressIndex } from '../core/indexer'\nimport type { TrigramIndex } from '../types'\nimport type { CompactProvince, CompactAmphure, CompactTambon } from './defaultData'\n\nlet cached: TrigramIndex | null = null\n\nexport function clearDefaultIndex(): void {\n cached = null\n}\n\nexport async function loadDefaultIndex(): Promise<TrigramIndex> {\n if (cached) return cached\n const { p, a, t } = await import('./defaultData') as {\n p: CompactProvince[]\n a: CompactAmphure[]\n t: CompactTambon[]\n }\n cached = buildThaiAddressIndex({\n provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),\n amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),\n tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code: Number(zip_code), deleted_at: null })),\n })\n return cached\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode)\n })\n return matches.slice(0, limit)\n }\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh) ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh) ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh)\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n","// NOTE (SZ-4): CJS consumers (`dist/index.cjs`) cannot tree-shake this hook out of the bundle.\n// If you are using the CJS build in a non-React environment, import only from the core entry\n// or use the ESM build with a bundler that supports tree-shaking.\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { searchThaiAddress } from '../core/search'\nimport { formatThaiAddressSuggestion } from '../core/formatter'\nimport { resolveThaiAddress } from '../core/resolver'\nimport type {\n UseThaiAddressAutocompleteOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n ResolvedThaiAddress,\n} from '../types'\n\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n const [query, setQueryState] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\n\n const setQuery = useCallback((value: string) => {\n setQueryState(value)\n }, [])\n\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(index, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\n\n /**\n * Resolves a suggestion to a full ThaiAddress and clears the suggestions list.\n *\n * Note: `query` is intentionally left unchanged so the input field retains the\n * typed text. Call `clear()` afterwards if you want to reset the input as well.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n throw new Error(`[thaizip] No record found for suggestion id \"${item.id}\". Make sure to use suggestions returned by this hook.`)\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQueryState('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n return {\n query,\n setQuery,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thaizip",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Fast, fuzzy Thai address autocomplete library (headless + React)",
5
5
  "keywords": [
6
6
  "thai",
@@ -43,7 +43,8 @@
43
43
  "generate-data": "tsx src/data/generate.ts",
44
44
  "test": "vitest run",
45
45
  "test:watch": "vitest",
46
- "test:coverage": "vitest run --coverage"
46
+ "test:coverage": "vitest run --coverage",
47
+ "typecheck": "tsc --noEmit"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@testing-library/react": "^16.3.2",
@@ -67,6 +68,6 @@
67
68
  }
68
69
  },
69
70
  "engines": {
70
- "node": ">=14"
71
+ "node": ">=18"
71
72
  }
72
73
  }