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 +58 -6
- package/dist/data.cjs +62 -33
- package/dist/data.d.cts +2 -1
- package/dist/data.d.ts +2 -1
- package/dist/data.js +62 -34
- package/dist/index.cjs +40 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -18
- package/dist/index.d.ts +6 -18
- package/dist/index.js +41 -88
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +188 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +72 -0
- package/dist/react.d.ts +72 -0
- package/dist/react.js +186 -0
- package/dist/react.js.map +1 -0
- package/package.json +7 -2
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
|
-
|
|
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
|
|
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 = /[
|
|
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
|
|
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)
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 = /[
|
|
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
|
|
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)
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 = /[
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|