thaizip 0.4.0 → 0.6.0

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
@@ -20,6 +20,16 @@ npm install thaizip
20
20
 
21
21
  ---
22
22
 
23
+ ## Package exports
24
+
25
+ | import path | เนื้อหา |
26
+ |---|---|
27
+ | `thaizip` | core functions + types ทั้งหมด (ไม่มี React) |
28
+ | `thaizip/react` | `useThaiAddressAutocomplete` hook + types |
29
+ | `thaizip/data` | `loadDefaultIndex`, `clearDefaultIndex` |
30
+
31
+ ---
32
+
23
33
  ## การใช้งานพื้นฐาน (Vanilla JS / TypeScript)
24
34
 
25
35
  ### โหลด index และค้นหาที่อยู่
@@ -97,13 +107,13 @@ const resolved = resolveThaiAddress(results[0])
97
107
 
98
108
  ### `useThaiAddressAutocomplete`
99
109
 
100
- Hook นี้จัดการ state ของ query, suggestions, debounce ให้ทั้งหมด
110
+ Hook นี้จัดการ state ของ query, suggestions, debounce ให้ทั้งหมด **import จาก `thaizip/react`**
101
111
 
102
112
  ```tsx
103
113
  import { useState, useEffect } from 'react'
104
114
  import { loadDefaultIndex } from 'thaizip/data'
105
- import { useThaiAddressAutocomplete } from 'thaizip'
106
- import type { TrigramIndex } from 'thaizip'
115
+ import { useThaiAddressAutocomplete } from 'thaizip/react'
116
+ import type { TrigramIndex, ResolvedThaiAddress } from 'thaizip/react'
107
117
 
108
118
  // แนะนำ: แยก loading และ form ออกจากกัน เพื่อให้ hook เรียกหลัง index พร้อมแล้วเสมอ
109
119
  function AddressPage() {
@@ -126,7 +136,7 @@ function AddressForm({ index }: { index: TrigramIndex }) {
126
136
  threshold: 0.4, // default: 0.4
127
137
  })
128
138
 
129
- const [address, setAddress] = useState(null)
139
+ const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)
130
140
 
131
141
  return (
132
142
  <div>
@@ -218,16 +228,30 @@ const index = buildThaiAddressIndex({
218
228
  amphures: [...],
219
229
  tambons: [...],
220
230
  // geographies ไม่จำเป็นต้องระบุ (ไม่ได้ใช้งานโดย indexer)
231
+ }, {
232
+ // รับ callback เมื่อ tambon ถูกข้ามเนื่องจากไม่พบ amphure/province
233
+ onSkip: (tambon) => console.warn('skipped tambon:', tambon.id),
221
234
  })
222
235
  ```
223
236
 
224
- รูปแบบข้อมูล raw ดูได้จาก type `RawData`, `RawProvince`, `RawAmphure`, `RawTambon` ที่ export มาจาก library
237
+ ### reset cache (สำหรับ test isolation)
238
+
239
+ ```ts
240
+ import { clearDefaultIndex } from 'thaizip/data'
241
+
242
+ clearDefaultIndex() // รีเซ็ต singleton cache ของ loadDefaultIndex
243
+ ```
244
+
245
+ รูปแบบข้อมูล raw ดูได้จาก type `RawData`, `RawProvince`, `RawAmphure`, `RawTambon` ที่ export มาจาก `thaizip`
225
246
 
226
247
  ---
227
248
 
228
249
  ## Types หลัก
229
250
 
251
+ Types ทั้งหมดสามารถ import ได้จาก `thaizip` หรือ `thaizip/react` (React-specific types)
252
+
230
253
  ```ts
254
+ // จาก thaizip หรือ thaizip/react
231
255
  type ThaiAddressSuggestion = {
232
256
  id: string
233
257
  label: string // "ตำบล > อำเภอ > จังหวัด XXXXX"
@@ -240,6 +264,7 @@ type ThaiAddressSuggestion = {
240
264
  zipCode: string
241
265
  }
242
266
 
267
+ // จาก thaizip หรือ thaizip/react
243
268
  type ResolvedThaiAddress = {
244
269
  tambon: string // alias: subdistrict
245
270
  tambonEn: string // alias: subdistrictEn
@@ -258,6 +283,33 @@ type ResolvedThaiAddress = {
258
283
 
259
284
  ---
260
285
 
286
+ ## Migration จาก v0.5.x → v1.0.0
287
+
288
+ ### hook ย้ายไปอยู่ที่ `thaizip/react`
289
+
290
+ ```ts
291
+ // v0.5.x
292
+ import { useThaiAddressAutocomplete } from 'thaizip'
293
+ import type { TrigramIndex } from 'thaizip'
294
+
295
+ // v1.0.0
296
+ import { useThaiAddressAutocomplete } from 'thaizip/react'
297
+ import type { TrigramIndex, ResolvedThaiAddress, ThaiAddressSuggestion } from 'thaizip/react'
298
+ // หรือยังคง import types จาก 'thaizip' ได้เช่นเดิม
299
+ ```
300
+
301
+ ### `clearDefaultIndex` ย้ายไปอยู่ที่ `thaizip/data`
302
+
303
+ ```ts
304
+ // v0.5.x
305
+ import { clearDefaultIndex } from 'thaizip'
306
+
307
+ // v1.0.0
308
+ import { clearDefaultIndex } from 'thaizip/data'
309
+ ```
310
+
311
+ ---
312
+
261
313
  ## Migration จาก v0.2.x → v0.3.0+
262
314
 
263
315
  ```ts
@@ -277,7 +329,7 @@ const results = searchThaiAddress(index, query)
277
329
 
278
330
  ข้อมูลอ้างอิงจากการแบ่งเขตการปกครองของไทย (77 จังหวัด, 920 อำเภอ, ~7,385 ตำบล)
279
331
 
280
- ตำบลที่ยังมีสถานะ active แต่อ้างอิงอำเภอที่ถูก soft-deleted จะถูกข้ามโดย index โดยอัตโนมัติ พร้อมแสดง `console.warn` ให้ทราบ
332
+ ตำบลที่ยังมีสถานะ active แต่อ้างอิงอำเภอหรือจังหวัดที่ถูก soft-deleted จะถูกข้ามโดย index โดยอัตโนมัติ ใช้ `onSkip` callback ใน `buildThaiAddressIndex` เพื่อรับแจ้ง
281
333
 
282
334
  ---
283
335
 
package/dist/data.cjs CHANGED
@@ -27,8 +27,8 @@ var init_defaultData = __esm({
27
27
  });
28
28
 
29
29
  // src/core/normalizer.ts
30
- var TONE_MARKS = /[\u0E48-\u0E4B\u0E47\u0E4C]/g;
31
- var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/;
30
+ var TONE_MARKS = /[่-๋็์]/g;
31
+ var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
32
32
  function normalizeThaiAddressText(input) {
33
33
  if (!input) return "";
34
34
  let text = input.trim();
@@ -56,7 +56,23 @@ function extractTrigramsNormalized(text) {
56
56
  }
57
57
 
58
58
  // src/core/indexer.ts
59
- function buildThaiAddressIndex(data) {
59
+ function addTrigrams(map, trigrams, idx) {
60
+ for (const trigram of trigrams) {
61
+ let set = map.get(trigram);
62
+ if (!set) {
63
+ set = /* @__PURE__ */ new Set();
64
+ map.set(trigram, set);
65
+ }
66
+ set.add(idx);
67
+ }
68
+ }
69
+ function combinedTrigrams(nameTh, nameEn) {
70
+ const s = /* @__PURE__ */ new Set();
71
+ for (const t2 of extractTrigrams(nameTh)) s.add(t2);
72
+ for (const t2 of extractTrigrams(nameEn)) s.add(t2);
73
+ return s;
74
+ }
75
+ function buildThaiAddressIndex(data, options) {
60
76
  const { provinces, amphures, tambons } = data;
61
77
  const provMap = new Map(
62
78
  provinces.filter((p2) => !p2.deleted_at).map((p2) => [p2.id, p2])
@@ -64,6 +80,14 @@ function buildThaiAddressIndex(data) {
64
80
  const ampMap = new Map(
65
81
  amphures.filter((a2) => !a2.deleted_at).map((a2) => [a2.id, a2])
66
82
  );
83
+ const provTrigrams = /* @__PURE__ */ new Map();
84
+ for (const [id, prov] of provMap) {
85
+ provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
86
+ }
87
+ const ampTrigrams = /* @__PURE__ */ new Map();
88
+ for (const [id, amp] of ampMap) {
89
+ ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
90
+ }
67
91
  const records = [];
68
92
  const map = /* @__PURE__ */ new Map();
69
93
  const zipIndex = /* @__PURE__ */ new Map();
@@ -71,13 +95,12 @@ function buildThaiAddressIndex(data) {
71
95
  if (tambon.deleted_at) continue;
72
96
  const amphure = ampMap.get(tambon.amphure_id);
73
97
  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
98
  continue;
78
99
  }
79
100
  const province = provMap.get(amphure.province_id);
80
- if (!province) continue;
101
+ if (!province) {
102
+ continue;
103
+ }
81
104
  const record = {
82
105
  provinceId: province.id,
83
106
  provinceNameTh: province.name_th,
@@ -95,40 +118,46 @@ function buildThaiAddressIndex(data) {
95
118
  const existing = zipIndex.get(record.zipCode);
96
119
  if (existing) existing.push(idx);
97
120
  else zipIndex.set(record.zipCode, [idx]);
98
- const fields = [
99
- record.tambonNameTh,
100
- record.tambonNameEn,
101
- record.amphureNameTh,
102
- record.amphureNameEn,
103
- record.provinceNameTh,
104
- record.provinceNameEn,
105
- record.zipCode
106
- ];
107
- for (const field of fields) {
108
- for (const trigram of extractTrigrams(field)) {
109
- let set = map.get(trigram);
110
- if (!set) {
111
- set = /* @__PURE__ */ new Set();
112
- map.set(trigram, set);
113
- }
114
- set.add(idx);
115
- }
116
- }
121
+ addTrigrams(map, extractTrigrams(record.tambonNameTh), idx);
122
+ addTrigrams(map, extractTrigrams(record.tambonNameEn), idx);
123
+ addTrigrams(map, extractTrigrams(record.zipCode), idx);
124
+ addTrigrams(map, provTrigrams.get(province.id), idx);
125
+ addTrigrams(map, ampTrigrams.get(amphure.id), idx);
117
126
  }
118
127
  return { map, records, zipIndex };
119
128
  }
120
129
 
121
130
  // src/data/loader.ts
122
131
  var cached = null;
132
+ var inflightPromise = null;
133
+ var loadGeneration = 0;
134
+ function clearDefaultIndex() {
135
+ cached = null;
136
+ inflightPromise = null;
137
+ loadGeneration++;
138
+ }
123
139
  async function loadDefaultIndex() {
124
140
  if (cached) return cached;
125
- const { p: p2, a: a2, t: t2 } = await Promise.resolve().then(() => (init_defaultData(), defaultData_exports));
126
- cached = buildThaiAddressIndex({
127
- provinces: p2.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
128
- amphures: a2.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
129
- tambons: t2.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 }))
130
- });
131
- return cached;
141
+ if (!inflightPromise) {
142
+ const gen = loadGeneration;
143
+ inflightPromise = Promise.resolve().then(() => (init_defaultData(), defaultData_exports)).then(({ p: p2, a: a2, t: t2 }) => {
144
+ const index = buildThaiAddressIndex({
145
+ provinces: p2.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
146
+ amphures: a2.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
147
+ tambons: t2.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
148
+ });
149
+ if (loadGeneration === gen) {
150
+ cached = index;
151
+ inflightPromise = null;
152
+ }
153
+ return index;
154
+ }).catch((err) => {
155
+ if (loadGeneration === gen) inflightPromise = null;
156
+ throw err;
157
+ });
158
+ }
159
+ return inflightPromise;
132
160
  }
133
161
 
162
+ exports.clearDefaultIndex = clearDefaultIndex;
134
163
  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
@@ -25,8 +25,8 @@ var init_defaultData = __esm({
25
25
  });
26
26
 
27
27
  // src/core/normalizer.ts
28
- var TONE_MARKS = /[\u0E48-\u0E4B\u0E47\u0E4C]/g;
29
- var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/;
28
+ var TONE_MARKS = /[่-๋็์]/g;
29
+ var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
30
30
  function normalizeThaiAddressText(input) {
31
31
  if (!input) return "";
32
32
  let text = input.trim();
@@ -54,7 +54,23 @@ function extractTrigramsNormalized(text) {
54
54
  }
55
55
 
56
56
  // src/core/indexer.ts
57
- function buildThaiAddressIndex(data) {
57
+ function addTrigrams(map, trigrams, idx) {
58
+ for (const trigram of trigrams) {
59
+ let set = map.get(trigram);
60
+ if (!set) {
61
+ set = /* @__PURE__ */ new Set();
62
+ map.set(trigram, set);
63
+ }
64
+ set.add(idx);
65
+ }
66
+ }
67
+ function combinedTrigrams(nameTh, nameEn) {
68
+ const s = /* @__PURE__ */ new Set();
69
+ for (const t2 of extractTrigrams(nameTh)) s.add(t2);
70
+ for (const t2 of extractTrigrams(nameEn)) s.add(t2);
71
+ return s;
72
+ }
73
+ function buildThaiAddressIndex(data, options) {
58
74
  const { provinces, amphures, tambons } = data;
59
75
  const provMap = new Map(
60
76
  provinces.filter((p2) => !p2.deleted_at).map((p2) => [p2.id, p2])
@@ -62,6 +78,14 @@ function buildThaiAddressIndex(data) {
62
78
  const ampMap = new Map(
63
79
  amphures.filter((a2) => !a2.deleted_at).map((a2) => [a2.id, a2])
64
80
  );
81
+ const provTrigrams = /* @__PURE__ */ new Map();
82
+ for (const [id, prov] of provMap) {
83
+ provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
84
+ }
85
+ const ampTrigrams = /* @__PURE__ */ new Map();
86
+ for (const [id, amp] of ampMap) {
87
+ ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
88
+ }
65
89
  const records = [];
66
90
  const map = /* @__PURE__ */ new Map();
67
91
  const zipIndex = /* @__PURE__ */ new Map();
@@ -69,13 +93,12 @@ function buildThaiAddressIndex(data) {
69
93
  if (tambon.deleted_at) continue;
70
94
  const amphure = ampMap.get(tambon.amphure_id);
71
95
  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
96
  continue;
76
97
  }
77
98
  const province = provMap.get(amphure.province_id);
78
- if (!province) continue;
99
+ if (!province) {
100
+ continue;
101
+ }
79
102
  const record = {
80
103
  provinceId: province.id,
81
104
  provinceNameTh: province.name_th,
@@ -93,40 +116,45 @@ function buildThaiAddressIndex(data) {
93
116
  const existing = zipIndex.get(record.zipCode);
94
117
  if (existing) existing.push(idx);
95
118
  else zipIndex.set(record.zipCode, [idx]);
96
- const fields = [
97
- record.tambonNameTh,
98
- record.tambonNameEn,
99
- record.amphureNameTh,
100
- record.amphureNameEn,
101
- record.provinceNameTh,
102
- record.provinceNameEn,
103
- record.zipCode
104
- ];
105
- for (const field of fields) {
106
- for (const trigram of extractTrigrams(field)) {
107
- let set = map.get(trigram);
108
- if (!set) {
109
- set = /* @__PURE__ */ new Set();
110
- map.set(trigram, set);
111
- }
112
- set.add(idx);
113
- }
114
- }
119
+ addTrigrams(map, extractTrigrams(record.tambonNameTh), idx);
120
+ addTrigrams(map, extractTrigrams(record.tambonNameEn), idx);
121
+ addTrigrams(map, extractTrigrams(record.zipCode), idx);
122
+ addTrigrams(map, provTrigrams.get(province.id), idx);
123
+ addTrigrams(map, ampTrigrams.get(amphure.id), idx);
115
124
  }
116
125
  return { map, records, zipIndex };
117
126
  }
118
127
 
119
128
  // src/data/loader.ts
120
129
  var cached = null;
130
+ var inflightPromise = null;
131
+ var loadGeneration = 0;
132
+ function clearDefaultIndex() {
133
+ cached = null;
134
+ inflightPromise = null;
135
+ loadGeneration++;
136
+ }
121
137
  async function loadDefaultIndex() {
122
138
  if (cached) return cached;
123
- const { p: p2, a: a2, t: t2 } = await Promise.resolve().then(() => (init_defaultData(), defaultData_exports));
124
- cached = buildThaiAddressIndex({
125
- provinces: p2.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
126
- amphures: a2.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
127
- tambons: t2.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 }))
128
- });
129
- return cached;
139
+ if (!inflightPromise) {
140
+ const gen = loadGeneration;
141
+ inflightPromise = Promise.resolve().then(() => (init_defaultData(), defaultData_exports)).then(({ p: p2, a: a2, t: t2 }) => {
142
+ const index = buildThaiAddressIndex({
143
+ provinces: p2.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
144
+ amphures: a2.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
145
+ tambons: t2.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
146
+ });
147
+ if (loadGeneration === gen) {
148
+ cached = index;
149
+ inflightPromise = null;
150
+ }
151
+ return index;
152
+ }).catch((err) => {
153
+ if (loadGeneration === gen) inflightPromise = null;
154
+ throw err;
155
+ });
156
+ }
157
+ return inflightPromise;
130
158
  }
131
159
 
132
- export { loadDefaultIndex };
160
+ export { clearDefaultIndex, loadDefaultIndex };
package/dist/index.cjs CHANGED
@@ -1,10 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var react = require('react');
4
-
5
3
  // src/core/normalizer.ts
6
- var TONE_MARKS = /[\u0E48-\u0E4B\u0E47\u0E4C]/g;
7
- var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/;
4
+ var TONE_MARKS = /[่-๋็์]/g;
5
+ var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
8
6
  function normalizeThaiAddressText(input) {
9
7
  if (!input) return "";
10
8
  let text = input.trim();
@@ -32,7 +30,23 @@ function extractTrigramsNormalized(text) {
32
30
  }
33
31
 
34
32
  // src/core/indexer.ts
35
- function buildThaiAddressIndex(data) {
33
+ function addTrigrams(map, trigrams, idx) {
34
+ for (const trigram of trigrams) {
35
+ let set = map.get(trigram);
36
+ if (!set) {
37
+ set = /* @__PURE__ */ new Set();
38
+ map.set(trigram, set);
39
+ }
40
+ set.add(idx);
41
+ }
42
+ }
43
+ function combinedTrigrams(nameTh, nameEn) {
44
+ const s = /* @__PURE__ */ new Set();
45
+ for (const t of extractTrigrams(nameTh)) s.add(t);
46
+ for (const t of extractTrigrams(nameEn)) s.add(t);
47
+ return s;
48
+ }
49
+ function buildThaiAddressIndex(data, options) {
36
50
  const { provinces, amphures, tambons } = data;
37
51
  const provMap = new Map(
38
52
  provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
@@ -40,6 +54,14 @@ function buildThaiAddressIndex(data) {
40
54
  const ampMap = new Map(
41
55
  amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
42
56
  );
57
+ const provTrigrams = /* @__PURE__ */ new Map();
58
+ for (const [id, prov] of provMap) {
59
+ provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
60
+ }
61
+ const ampTrigrams = /* @__PURE__ */ new Map();
62
+ for (const [id, amp] of ampMap) {
63
+ ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
64
+ }
43
65
  const records = [];
44
66
  const map = /* @__PURE__ */ new Map();
45
67
  const zipIndex = /* @__PURE__ */ new Map();
@@ -47,13 +69,14 @@ function buildThaiAddressIndex(data) {
47
69
  if (tambon.deleted_at) continue;
48
70
  const amphure = ampMap.get(tambon.amphure_id);
49
71
  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
- );
72
+ if (options?.onSkip) options.onSkip(tambon);
53
73
  continue;
54
74
  }
55
75
  const province = provMap.get(amphure.province_id);
56
- if (!province) continue;
76
+ if (!province) {
77
+ if (options?.onSkip) options.onSkip(tambon);
78
+ continue;
79
+ }
57
80
  const record = {
58
81
  provinceId: province.id,
59
82
  provinceNameTh: province.name_th,
@@ -71,25 +94,11 @@ function buildThaiAddressIndex(data) {
71
94
  const existing = zipIndex.get(record.zipCode);
72
95
  if (existing) existing.push(idx);
73
96
  else zipIndex.set(record.zipCode, [idx]);
74
- const fields = [
75
- record.tambonNameTh,
76
- record.tambonNameEn,
77
- record.amphureNameTh,
78
- record.amphureNameEn,
79
- record.provinceNameTh,
80
- record.provinceNameEn,
81
- record.zipCode
82
- ];
83
- for (const field of fields) {
84
- for (const trigram of extractTrigrams(field)) {
85
- let set = map.get(trigram);
86
- if (!set) {
87
- set = /* @__PURE__ */ new Set();
88
- map.set(trigram, set);
89
- }
90
- set.add(idx);
91
- }
92
- }
97
+ addTrigrams(map, extractTrigrams(record.tambonNameTh), idx);
98
+ addTrigrams(map, extractTrigrams(record.tambonNameEn), idx);
99
+ addTrigrams(map, extractTrigrams(record.zipCode), idx);
100
+ addTrigrams(map, provTrigrams.get(province.id), idx);
101
+ addTrigrams(map, ampTrigrams.get(amphure.id), idx);
93
102
  }
94
103
  return { map, records, zipIndex };
95
104
  }
@@ -114,12 +123,12 @@ function searchThaiAddress(index, query, options) {
114
123
  matches.sort((a, b) => {
115
124
  if (a.zipCode === normalized && b.zipCode !== normalized) return -1;
116
125
  if (b.zipCode === normalized && a.zipCode !== normalized) return 1;
117
- return a.zipCode.localeCompare(b.zipCode);
126
+ return a.zipCode.localeCompare(b.zipCode, void 0, { numeric: true });
118
127
  });
119
128
  return matches.slice(0, limit);
120
129
  }
130
+ if (normalized.length < 3 && query.trim().length < 3) return [];
121
131
  const queryTrigrams = extractTrigramsNormalized(normalized);
122
- if (queryTrigrams.size === 0) return [];
123
132
  const hits = /* @__PURE__ */ new Map();
124
133
  for (const trigram of queryTrigrams) {
125
134
  const candidates = index.map.get(trigram);
@@ -139,7 +148,7 @@ function searchThaiAddress(index, query, options) {
139
148
  if (b.score !== a.score) return b.score - a.score;
140
149
  const ra = index.records[a.idx];
141
150
  const rb = index.records[b.idx];
142
- return ra.provinceNameTh.localeCompare(rb.provinceNameTh) || ra.amphureNameTh.localeCompare(rb.amphureNameTh) || ra.tambonNameTh.localeCompare(rb.tambonNameTh);
151
+ return ra.provinceNameTh.localeCompare(rb.provinceNameTh, "th") || ra.amphureNameTh.localeCompare(rb.amphureNameTh, "th") || ra.tambonNameTh.localeCompare(rb.tambonNameTh, "th");
143
152
  });
144
153
  return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
145
154
  }
@@ -176,68 +185,11 @@ function resolveThaiAddress(record) {
176
185
  postalCode: record.zipCode
177
186
  };
178
187
  }
179
- function useThaiAddressAutocomplete(options) {
180
- const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
181
- const [query, setQueryState] = react.useState("");
182
- const [suggestions, setSuggestions] = react.useState([]);
183
- const timerRef = react.useRef(null);
184
- const matchedRecordsMapRef = react.useRef(/* @__PURE__ */ new Map());
185
- const setQuery = react.useCallback((value) => {
186
- setQueryState(value);
187
- }, []);
188
- react.useEffect(() => {
189
- if (timerRef.current) clearTimeout(timerRef.current);
190
- if (query.length === 0) {
191
- setSuggestions([]);
192
- matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
193
- return;
194
- }
195
- timerRef.current = setTimeout(() => {
196
- const records = searchThaiAddress(index, query, { limit, threshold });
197
- const formatted = records.map(formatThaiAddressSuggestion);
198
- const recordMap = /* @__PURE__ */ new Map();
199
- for (let i = 0; i < records.length; i++) {
200
- recordMap.set(formatted[i].id, records[i]);
201
- }
202
- matchedRecordsMapRef.current = recordMap;
203
- setSuggestions(formatted);
204
- }, debounceMs);
205
- return () => {
206
- if (timerRef.current) clearTimeout(timerRef.current);
207
- };
208
- }, [query, index, limit, threshold, debounceMs]);
209
- const selectSuggestion = react.useCallback(
210
- (item) => {
211
- const record = matchedRecordsMapRef.current.get(item.id);
212
- if (!record) {
213
- throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
214
- }
215
- setSuggestions([]);
216
- matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
217
- return resolveThaiAddress(record);
218
- },
219
- []
220
- );
221
- const clear = react.useCallback(() => {
222
- setQueryState("");
223
- setSuggestions([]);
224
- matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
225
- }, []);
226
- return {
227
- query,
228
- setQuery,
229
- suggestions,
230
- isOpen: query.length > 0 && suggestions.length > 0,
231
- selectSuggestion,
232
- clear
233
- };
234
- }
235
188
 
236
189
  exports.buildThaiAddressIndex = buildThaiAddressIndex;
237
190
  exports.formatThaiAddressSuggestion = formatThaiAddressSuggestion;
238
191
  exports.normalizeThaiAddressText = normalizeThaiAddressText;
239
192
  exports.resolveThaiAddress = resolveThaiAddress;
240
193
  exports.searchThaiAddress = searchThaiAddress;
241
- exports.useThaiAddressAutocomplete = useThaiAddressAutocomplete;
242
194
  //# sourceMappingURL=index.cjs.map
243
195
  //# sourceMappingURL=index.cjs.map