thaizip 0.6.0 → 0.7.2
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 +90 -265
- package/dist/data.cjs +53 -4
- package/dist/data.d.cts +16 -0
- package/dist/data.d.ts +16 -0
- package/dist/data.js +53 -4
- package/dist/index.cjs +293 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -2
- package/dist/index.d.ts +156 -2
- package/dist/index.js +288 -22
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +228 -42
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +40 -1
- package/dist/react.d.ts +40 -1
- package/dist/react.js +229 -43
- package/dist/react.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,337 +1,162 @@
|
|
|
1
1
|
# thaizip
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- ค้นหาได้ทั้งภาษาไทย ภาษาอังกฤษ และรหัสไปรษณีย์
|
|
6
|
-
- Fuzzy search — ไม่ต้องใส่วรรณยุกต์ก็ค้นหาได้ เช่น "ลาดพราว" เจอ "ลาดพร้าว"
|
|
7
|
-
- ไม่มี dependency นอกจาก React (optional)
|
|
8
|
-
- โหลดข้อมูลแบบ async — ไม่บล็อก initial bundle (~132 KB gzip)
|
|
9
|
-
- รองรับ ESM และ CJS
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## ติดตั้ง
|
|
3
|
+
Fast fuzzy autocomplete for Thai addresses — subdistrict, district, province, postal code. Thai and English input, zero runtime dependencies, React optional.
|
|
14
4
|
|
|
15
5
|
```bash
|
|
16
6
|
npm install thaizip
|
|
17
7
|
```
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Package exports
|
|
24
|
-
|
|
25
|
-
| import path | เนื้อหา |
|
|
9
|
+
| import path | contents |
|
|
26
10
|
|---|---|
|
|
27
|
-
| `thaizip` | core functions + types
|
|
28
|
-
| `thaizip/react` | `useThaiAddressAutocomplete`
|
|
29
|
-
| `thaizip/data` | `loadDefaultIndex`, `clearDefaultIndex` |
|
|
30
|
-
|
|
31
|
-
---
|
|
11
|
+
| `thaizip` | core functions + types (no React code, 4.4 KB gzip) |
|
|
12
|
+
| `thaizip/react` | `useThaiAddressAutocomplete` (ships `"use client"`) |
|
|
13
|
+
| `thaizip/data` | `loadDefaultIndex`, `clearDefaultIndex` (132 KB gzip) |
|
|
32
14
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
### โหลด index และค้นหาที่อยู่
|
|
15
|
+
## Quick start
|
|
36
16
|
|
|
37
17
|
```ts
|
|
38
18
|
import { loadDefaultIndex } from 'thaizip/data'
|
|
39
|
-
import { searchThaiAddress } from 'thaizip'
|
|
40
|
-
|
|
41
|
-
// โหลด index ครั้งแรก ~200ms (cached หลังจากนั้น)
|
|
42
|
-
const index = await loadDefaultIndex()
|
|
19
|
+
import { searchThaiAddress, formatThaiAddressSuggestion, resolveThaiAddress } from 'thaizip'
|
|
43
20
|
|
|
44
|
-
//
|
|
45
|
-
const results = searchThaiAddress(index, 'ลาดพร้าว')
|
|
21
|
+
const index = await loadDefaultIndex() // ~40ms of synchronous work, cached after
|
|
46
22
|
|
|
47
|
-
|
|
48
|
-
|
|
23
|
+
searchThaiAddress(index, 'ลาดพร้าว')
|
|
24
|
+
searchThaiAddress(index, 'bang rak')
|
|
25
|
+
searchThaiAddress(index, '10500')
|
|
49
26
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// ⚠️ ไม่รองรับ: ค้นหาแบบรวม text + รหัสไปรษณีย์ในครั้งเดียว (เช่น "ลาดพร้าว 10900")
|
|
54
|
-
// ให้ค้นหาด้วยชื่อสถานที่ หรือรหัสไปรษณีย์ แยกกัน
|
|
55
|
-
|
|
56
|
-
console.log(results)
|
|
57
|
-
// [{ tambonId, tambonNameTh, tambonNameEn, amphureId, amphureNameTh, ... zipCode }, ...]
|
|
27
|
+
formatThaiAddressSuggestion(record) // for the dropdown
|
|
28
|
+
formatThaiAddressSuggestion(record, { locale: 'en' })
|
|
29
|
+
resolveThaiAddress(record) // for saving
|
|
58
30
|
```
|
|
59
31
|
|
|
60
|
-
|
|
32
|
+
Call `loadDefaultIndex()` at mount or route load — deferring it to the user's first keystroke costs ~2 dropped frames.
|
|
33
|
+
|
|
34
|
+
## Search
|
|
61
35
|
|
|
62
36
|
```ts
|
|
63
|
-
|
|
64
|
-
limit:
|
|
65
|
-
threshold: 0.4,
|
|
37
|
+
searchThaiAddress(index, query, {
|
|
38
|
+
limit: 10, // text queries only
|
|
39
|
+
threshold: 0.4, // match quality 0–1
|
|
40
|
+
zipLimit: Infinity, // all-digit queries — unlimited by default
|
|
41
|
+
romanizationAliases: true, // expand non-RTGS spellings
|
|
66
42
|
})
|
|
67
43
|
```
|
|
68
44
|
|
|
69
|
-
|
|
45
|
+
Results rank by trigram score, then by how well the query matches the subdistrict's **own** name (exact → prefix → substring), then alphabetically. That second key is what keeps ตำบลลาดพร้าว above the other subdistricts that merely sit inside เขตลาดพร้าว.
|
|
70
46
|
|
|
71
|
-
|
|
47
|
+
> Combined text + zip in one query (`"ลาดพร้าว 10900"`) is not supported — search by name or zip separately.
|
|
72
48
|
|
|
73
|
-
|
|
74
|
-
import { formatThaiAddressSuggestion } from 'thaizip'
|
|
75
|
-
|
|
76
|
-
const suggestion = formatThaiAddressSuggestion(results[0])
|
|
77
|
-
// {
|
|
78
|
-
// id: '100101',
|
|
79
|
-
// label: 'ลาดพร้าว > ลาดพร้าว > กรุงเทพมหานคร 10230',
|
|
80
|
-
// tambon: 'ลาดพร้าว', tambonEn: 'Lat Phrao',
|
|
81
|
-
// amphure: 'ลาดพร้าว', amphureEn: 'Lat Phrao',
|
|
82
|
-
// province: 'กรุงเทพมหานคร', provinceEn: 'Bangkok',
|
|
83
|
-
// zipCode: '10230',
|
|
84
|
-
// }
|
|
85
|
-
```
|
|
49
|
+
### Postal codes
|
|
86
50
|
|
|
87
|
-
|
|
51
|
+
One Thai postal code can cover many subdistricts — `45000` covers 33, and 230 of the 953 codes cover more than 10 — so `zipLimit` defaults to unlimited rather than silently truncating at `limit`.
|
|
88
52
|
|
|
89
53
|
```ts
|
|
90
|
-
import {
|
|
91
|
-
|
|
92
|
-
const resolved = resolveThaiAddress(results[0])
|
|
93
|
-
// {
|
|
94
|
-
// tambon: 'ลาดพร้าว', tambonEn: 'Lat Phrao',
|
|
95
|
-
// amphure: 'ลาดพร้าว', amphureEn: 'Lat Phrao',
|
|
96
|
-
// province: 'กรุงเทพมหานคร', provinceEn: 'Bangkok',
|
|
97
|
-
// zipCode: '10230',
|
|
98
|
-
// subdistrict: 'ลาดพร้าว', subdistrictEn: 'Lat Phrao',
|
|
99
|
-
// district: 'ลาดพร้าว', districtEn: 'Lat Phrao',
|
|
100
|
-
// postalCode: '10230',
|
|
101
|
-
// }
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## การใช้งานกับ React
|
|
54
|
+
import { lookupByZipCode } from 'thaizip'
|
|
107
55
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
56
|
+
lookupByZipCode(index, '45000') // exact
|
|
57
|
+
lookupByZipCode(index, '450') // prefix
|
|
58
|
+
```
|
|
111
59
|
|
|
112
|
-
|
|
113
|
-
import { useState, useEffect } from 'react'
|
|
114
|
-
import { loadDefaultIndex } from 'thaizip/data'
|
|
115
|
-
import { useThaiAddressAutocomplete } from 'thaizip/react'
|
|
116
|
-
import type { TrigramIndex, ResolvedThaiAddress } from 'thaizip/react'
|
|
60
|
+
### English input
|
|
117
61
|
|
|
118
|
-
|
|
119
|
-
function AddressPage() {
|
|
120
|
-
const [index, setIndex] = useState<TrigramIndex | null>(null)
|
|
62
|
+
The dataset indexes official RTGS spellings, so `bang rak` and `chatuchak` work directly. 84 common non-RTGS spellings are mapped on first: `lardprao` → `lat phrao`, `ladkrabang` → `lat krabang`, `krungthep` → `bangkok`. It is a curated dictionary, not a transliterator — unlisted spellings still miss. Opt out with `romanizationAliases: false`.
|
|
121
63
|
|
|
122
|
-
|
|
123
|
-
loadDefaultIndex().then(setIndex)
|
|
124
|
-
}, [])
|
|
64
|
+
### Cascade dropdowns
|
|
125
65
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
66
|
+
```ts
|
|
67
|
+
import { listProvinces, listAmphures, listTambons } from 'thaizip'
|
|
129
68
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
index,
|
|
134
|
-
limit: 10, // default: 10
|
|
135
|
-
debounce: 200, // default: 200ms
|
|
136
|
-
threshold: 0.4, // default: 0.4
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<div>
|
|
143
|
-
<input
|
|
144
|
-
value={query}
|
|
145
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
146
|
-
placeholder="พิมพ์ตำบล อำเภอ จังหวัด หรือรหัสไปรษณีย์"
|
|
147
|
-
/>
|
|
148
|
-
|
|
149
|
-
{isOpen && (
|
|
150
|
-
<ul>
|
|
151
|
-
{suggestions.map((s) => (
|
|
152
|
-
<li key={s.id} onClick={() => {
|
|
153
|
-
const resolved = selectSuggestion(s)
|
|
154
|
-
setAddress(resolved)
|
|
155
|
-
}}>
|
|
156
|
-
{s.label}
|
|
157
|
-
</li>
|
|
158
|
-
))}
|
|
159
|
-
</ul>
|
|
160
|
-
)}
|
|
161
|
-
|
|
162
|
-
{address && (
|
|
163
|
-
<div>
|
|
164
|
-
<p>ตำบล: {address.tambon} ({address.tambonEn})</p>
|
|
165
|
-
<p>อำเภอ: {address.amphure} ({address.amphureEn})</p>
|
|
166
|
-
<p>จังหวัด: {address.province} ({address.provinceEn})</p>
|
|
167
|
-
<p>รหัสไปรษณีย์: {address.zipCode}</p>
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
</div>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
69
|
+
listProvinces(index) // 77, sorted by Thai name
|
|
70
|
+
listAmphures(index, provinceId)
|
|
71
|
+
listTambons(index, amphureId) // each with its zipCode
|
|
173
72
|
```
|
|
174
73
|
|
|
175
|
-
|
|
74
|
+
Backed by pre-built groupings, not scans over all 7,385 records. Unknown ids return `[]`.
|
|
176
75
|
|
|
177
|
-
|
|
178
|
-
|---|---|---|
|
|
179
|
-
| `query` | `string` | ข้อความที่ผู้ใช้พิมพ์อยู่ |
|
|
180
|
-
| `setQuery` | `(value: string) => void` | อัปเดต query |
|
|
181
|
-
| `suggestions` | `ThaiAddressSuggestion[]` | รายการผลลัพธ์ที่แสดงใน dropdown |
|
|
182
|
-
| `isOpen` | `boolean` | `true` เมื่อมี query และมี suggestions |
|
|
183
|
-
| `selectSuggestion` | `(item) => ResolvedThaiAddress` | เลือก suggestion แล้วได้ข้อมูลที่อยู่ครบ |
|
|
184
|
-
| `clear` | `() => void` | ล้าง query และ suggestions ทั้งหมด |
|
|
76
|
+
## React
|
|
185
77
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
## การใช้งานกับ Node.js / Express
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
import express from 'express'
|
|
192
|
-
import { loadDefaultIndex } from 'thaizip/data'
|
|
193
|
-
import { searchThaiAddress, formatThaiAddressSuggestion } from 'thaizip'
|
|
78
|
+
```tsx
|
|
79
|
+
import { useThaiAddressAutocomplete } from 'thaizip/react'
|
|
194
80
|
|
|
195
|
-
const
|
|
81
|
+
const { query, setQuery, setQuerySilent, suggestions, isOpen, selectSuggestion, clear } =
|
|
82
|
+
useThaiAddressAutocomplete({ index })
|
|
83
|
+
```
|
|
196
84
|
|
|
197
|
-
|
|
198
|
-
const index = await loadDefaultIndex()
|
|
85
|
+
Options: `index`, `limit`, `debounce` (200ms), `threshold`, `zipLimit`, `locale`, `initialQuery`, `onSelect`.
|
|
199
86
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
87
|
+
| value | description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `query` / `setQuery` | input value; `setQuery` searches |
|
|
90
|
+
| `setQuerySilent` | set the value **without** searching or reopening the dropdown |
|
|
91
|
+
| `suggestions` | `ThaiAddressSuggestion[]` |
|
|
92
|
+
| `isOpen` | `query` is non-empty and suggestions exist |
|
|
93
|
+
| `selectSuggestion` | `(item) => ResolvedThaiAddress \| null` — clears suggestions, leaves `query` alone; `null` if the item is stale |
|
|
94
|
+
| `clear` | reset query and suggestions |
|
|
203
95
|
|
|
204
|
-
|
|
205
|
-
res.json(results.map(formatThaiAddressSuggestion))
|
|
206
|
-
})
|
|
96
|
+
### Controlled forms
|
|
207
97
|
|
|
208
|
-
|
|
209
|
-
```
|
|
98
|
+
`selectSuggestion` leaves `query` untouched by design. To echo the choice back into the input use `setQuerySilent` — plain `setQuery` would immediately reopen the dropdown:
|
|
210
99
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
100
|
+
```tsx
|
|
101
|
+
const handlePick = (s: ThaiAddressSuggestion) => {
|
|
102
|
+
const address = selectSuggestion(s)
|
|
103
|
+
if (!address) return
|
|
104
|
+
setQuerySilent(s.label)
|
|
105
|
+
form.setValue('address', address)
|
|
106
|
+
}
|
|
215
107
|
```
|
|
216
108
|
|
|
217
|
-
|
|
109
|
+
Editing a saved address? Pass `initialQuery` rather than calling `setQuery` in an effect.
|
|
218
110
|
|
|
219
|
-
|
|
111
|
+
### Accessibility
|
|
220
112
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
```ts
|
|
224
|
-
import { buildThaiAddressIndex } from 'thaizip'
|
|
225
|
-
|
|
226
|
-
const index = buildThaiAddressIndex({
|
|
227
|
-
provinces: [...],
|
|
228
|
-
amphures: [...],
|
|
229
|
-
tambons: [...],
|
|
230
|
-
// geographies ไม่จำเป็นต้องระบุ (ไม่ได้ใช้งานโดย indexer)
|
|
231
|
-
}, {
|
|
232
|
-
// รับ callback เมื่อ tambon ถูกข้ามเนื่องจากไม่พบ amphure/province
|
|
233
|
-
onSkip: (tambon) => console.warn('skipped tambon:', tambon.id),
|
|
234
|
-
})
|
|
235
|
-
```
|
|
113
|
+
The hook owns query and suggestion state only. Keyboard navigation and ARIA combobox semantics are yours — `npx react-thaizip add autocomplete` scaffolds a component that already has them.
|
|
236
114
|
|
|
237
|
-
|
|
115
|
+
## Custom data
|
|
238
116
|
|
|
239
117
|
```ts
|
|
240
|
-
import {
|
|
118
|
+
import { buildThaiAddressIndex, validateRawData } from 'thaizip'
|
|
241
119
|
|
|
242
|
-
|
|
120
|
+
const index = buildThaiAddressIndex({ provinces, amphures, tambons }, {
|
|
121
|
+
onSkip: (tambon) => console.warn('skipped:', tambon.id),
|
|
122
|
+
validate: true, // default
|
|
123
|
+
})
|
|
243
124
|
```
|
|
244
125
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
---
|
|
126
|
+
Input is validated by default, so a stray non-string field fails with `[thaizip] tambon 100404: expected string for name_th, got number` instead of crashing inside the normalizer. It costs nothing measurable on a full-size dataset. `validateRawData(data)` runs the same checks standalone.
|
|
248
127
|
|
|
249
|
-
|
|
128
|
+
Types: `RawData`, `RawProvince`, `RawAmphure`, `RawTambon`.
|
|
250
129
|
|
|
251
|
-
Types
|
|
130
|
+
## Types
|
|
252
131
|
|
|
253
132
|
```ts
|
|
254
|
-
// จาก thaizip หรือ thaizip/react
|
|
255
133
|
type ThaiAddressSuggestion = {
|
|
256
134
|
id: string
|
|
257
|
-
label: string
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
amphureEn: string
|
|
262
|
-
province: string
|
|
263
|
-
provinceEn: string
|
|
135
|
+
label: string // follows `locale`, Thai by default
|
|
136
|
+
labelTh: string
|
|
137
|
+
labelEn: string
|
|
138
|
+
tambon: string; tambonEn: string
|
|
139
|
+
amphure: string; amphureEn: string
|
|
140
|
+
province: string; provinceEn: string
|
|
264
141
|
zipCode: string
|
|
265
142
|
}
|
|
266
143
|
|
|
267
|
-
// จาก thaizip หรือ thaizip/react
|
|
268
144
|
type ResolvedThaiAddress = {
|
|
269
|
-
tambon: string
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
zipCode: string // alias: postalCode
|
|
276
|
-
subdistrict: string
|
|
277
|
-
subdistrictEn: string
|
|
278
|
-
district: string
|
|
279
|
-
districtEn: string
|
|
145
|
+
tambon: string; tambonEn: string // alias: subdistrict / subdistrictEn
|
|
146
|
+
amphure: string; amphureEn: string // alias: district / districtEn
|
|
147
|
+
province: string; provinceEn: string
|
|
148
|
+
zipCode: string // alias: postalCode
|
|
149
|
+
subdistrict: string; subdistrictEn: string
|
|
150
|
+
district: string; districtEn: string
|
|
280
151
|
postalCode: string
|
|
281
152
|
}
|
|
282
153
|
```
|
|
283
154
|
|
|
284
|
-
|
|
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
|
-
|
|
313
|
-
## Migration จาก v0.2.x → v0.3.0+
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
// v0.2.x (sync, bundle ~630KB)
|
|
317
|
-
import { defaultIndex } from 'thaizip/data'
|
|
318
|
-
const results = searchThaiAddress(defaultIndex, query)
|
|
319
|
-
|
|
320
|
-
// v0.3.0+ (async, bundle ~132KB gzip)
|
|
321
|
-
import { loadDefaultIndex } from 'thaizip/data'
|
|
322
|
-
const index = await loadDefaultIndex() // cache อัตโนมัติ หลังจากโหลดครั้งแรก
|
|
323
|
-
const results = searchThaiAddress(index, query)
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## ข้อมูลที่อยู่
|
|
329
|
-
|
|
330
|
-
ข้อมูลอ้างอิงจากการแบ่งเขตการปกครองของไทย (77 จังหวัด, 920 อำเภอ, ~7,385 ตำบล)
|
|
155
|
+
## Data
|
|
331
156
|
|
|
332
|
-
|
|
157
|
+
77 provinces, 918 districts, 7,385 subdistricts. Records whose parent district or province is soft-deleted are excluded automatically.
|
|
333
158
|
|
|
334
|
-
|
|
159
|
+
`searchThaiAddress` is pure and framework-free — it works unchanged in Node, Vue, Svelte, or vanilla JS.
|
|
335
160
|
|
|
336
161
|
## License
|
|
337
162
|
|
package/dist/data.cjs
CHANGED
|
@@ -56,6 +56,36 @@ function extractTrigramsNormalized(text) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/core/indexer.ts
|
|
59
|
+
function typeErr(table, id, field, expected, value) {
|
|
60
|
+
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
61
|
+
}
|
|
62
|
+
function validateRawData(data) {
|
|
63
|
+
const { provinces, amphures, tambons } = data;
|
|
64
|
+
for (let i = 0; i < provinces.length; i++) {
|
|
65
|
+
const p2 = provinces[i];
|
|
66
|
+
if (typeof p2.id !== "number") throw typeErr("province", p2.id, "id", "number", p2.id);
|
|
67
|
+
if (typeof p2.name_th !== "string") throw typeErr("province", p2.id, "name_th", "string", p2.name_th);
|
|
68
|
+
if (typeof p2.name_en !== "string") throw typeErr("province", p2.id, "name_en", "string", p2.name_en);
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < amphures.length; i++) {
|
|
71
|
+
const a2 = amphures[i];
|
|
72
|
+
if (typeof a2.id !== "number") throw typeErr("amphure", a2.id, "id", "number", a2.id);
|
|
73
|
+
if (typeof a2.name_th !== "string") throw typeErr("amphure", a2.id, "name_th", "string", a2.name_th);
|
|
74
|
+
if (typeof a2.name_en !== "string") throw typeErr("amphure", a2.id, "name_en", "string", a2.name_en);
|
|
75
|
+
if (typeof a2.province_id !== "number") throw typeErr("amphure", a2.id, "province_id", "number", a2.province_id);
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < tambons.length; i++) {
|
|
78
|
+
const t2 = tambons[i];
|
|
79
|
+
if (typeof t2.id !== "number") throw typeErr("tambon", t2.id, "id", "number", t2.id);
|
|
80
|
+
if (typeof t2.name_th !== "string") throw typeErr("tambon", t2.id, "name_th", "string", t2.name_th);
|
|
81
|
+
if (typeof t2.name_en !== "string") throw typeErr("tambon", t2.id, "name_en", "string", t2.name_en);
|
|
82
|
+
const zipType = typeof t2.zip_code;
|
|
83
|
+
if (zipType !== "string" && zipType !== "number") {
|
|
84
|
+
throw typeErr("tambon", t2.id, "zip_code", "string or number", t2.zip_code);
|
|
85
|
+
}
|
|
86
|
+
if (typeof t2.amphure_id !== "number") throw typeErr("tambon", t2.id, "amphure_id", "number", t2.amphure_id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
59
89
|
function addTrigrams(map, trigrams, idx) {
|
|
60
90
|
for (const trigram of trigrams) {
|
|
61
91
|
let set = map.get(trigram);
|
|
@@ -73,6 +103,9 @@ function combinedTrigrams(nameTh, nameEn) {
|
|
|
73
103
|
return s;
|
|
74
104
|
}
|
|
75
105
|
function buildThaiAddressIndex(data, options) {
|
|
106
|
+
if (options?.validate !== false) {
|
|
107
|
+
validateRawData(data);
|
|
108
|
+
}
|
|
76
109
|
const { provinces, amphures, tambons } = data;
|
|
77
110
|
const provMap = new Map(
|
|
78
111
|
provinces.filter((p2) => !p2.deleted_at).map((p2) => [p2.id, p2])
|
|
@@ -91,14 +124,20 @@ function buildThaiAddressIndex(data, options) {
|
|
|
91
124
|
const records = [];
|
|
92
125
|
const map = /* @__PURE__ */ new Map();
|
|
93
126
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
127
|
+
const normTambon = [];
|
|
128
|
+
const normTambonEn = [];
|
|
129
|
+
const byProvince = /* @__PURE__ */ new Map();
|
|
130
|
+
const byAmphure = /* @__PURE__ */ new Map();
|
|
94
131
|
for (const tambon of tambons) {
|
|
95
132
|
if (tambon.deleted_at) continue;
|
|
96
133
|
const amphure = ampMap.get(tambon.amphure_id);
|
|
97
134
|
if (!amphure) {
|
|
135
|
+
if (options?.onSkip) options.onSkip(tambon);
|
|
98
136
|
continue;
|
|
99
137
|
}
|
|
100
138
|
const province = provMap.get(amphure.province_id);
|
|
101
139
|
if (!province) {
|
|
140
|
+
if (options?.onSkip) options.onSkip(tambon);
|
|
102
141
|
continue;
|
|
103
142
|
}
|
|
104
143
|
const record = {
|
|
@@ -118,13 +157,23 @@ function buildThaiAddressIndex(data, options) {
|
|
|
118
157
|
const existing = zipIndex.get(record.zipCode);
|
|
119
158
|
if (existing) existing.push(idx);
|
|
120
159
|
else zipIndex.set(record.zipCode, [idx]);
|
|
121
|
-
|
|
122
|
-
|
|
160
|
+
const provList = byProvince.get(province.id);
|
|
161
|
+
if (provList) provList.push(idx);
|
|
162
|
+
else byProvince.set(province.id, [idx]);
|
|
163
|
+
const ampList = byAmphure.get(amphure.id);
|
|
164
|
+
if (ampList) ampList.push(idx);
|
|
165
|
+
else byAmphure.set(amphure.id, [idx]);
|
|
166
|
+
const normTh = normalizeThaiAddressText(record.tambonNameTh);
|
|
167
|
+
normTambon.push(normTh);
|
|
168
|
+
addTrigrams(map, extractTrigramsNormalized(normTh), idx);
|
|
169
|
+
const normEn = normalizeThaiAddressText(record.tambonNameEn);
|
|
170
|
+
normTambonEn.push(normEn);
|
|
171
|
+
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
123
172
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
124
173
|
addTrigrams(map, provTrigrams.get(province.id), idx);
|
|
125
174
|
addTrigrams(map, ampTrigrams.get(amphure.id), idx);
|
|
126
175
|
}
|
|
127
|
-
return { map, records, zipIndex };
|
|
176
|
+
return { map, records, zipIndex, normTambon, normTambonEn, byProvince, byAmphure };
|
|
128
177
|
}
|
|
129
178
|
|
|
130
179
|
// src/data/loader.ts
|
|
@@ -145,7 +194,7 @@ async function loadDefaultIndex() {
|
|
|
145
194
|
provinces: p2.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
|
|
146
195
|
amphures: a2.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
|
|
147
196
|
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
|
-
});
|
|
197
|
+
}, { validate: false });
|
|
149
198
|
if (loadGeneration === gen) {
|
|
150
199
|
cached = index;
|
|
151
200
|
inflightPromise = null;
|
package/dist/data.d.cts
CHANGED
|
@@ -14,6 +14,22 @@ type TrigramIndex = {
|
|
|
14
14
|
map: Map<string, Set<number>>;
|
|
15
15
|
records: ThaiAddressRecord[];
|
|
16
16
|
zipIndex: Map<string, number[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Normalized Thai tambon names, parallel to `records` (same index).
|
|
19
|
+
* Used by the search ranker for exact/prefix match detection without
|
|
20
|
+
* re-normalizing on every query.
|
|
21
|
+
*/
|
|
22
|
+
normTambon: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Normalized English (RTGS) tambon names, parallel to `records`. Needed so
|
|
25
|
+
* romanized queries get the same own-name-beats-parent-name ranking that
|
|
26
|
+
* Thai queries get.
|
|
27
|
+
*/
|
|
28
|
+
normTambonEn: string[];
|
|
29
|
+
/** Record indices grouped by `provinceId`, in insertion order. */
|
|
30
|
+
byProvince: Map<number, number[]>;
|
|
31
|
+
/** Record indices grouped by `amphureId`, in insertion order. */
|
|
32
|
+
byAmphure: Map<number, number[]>;
|
|
17
33
|
};
|
|
18
34
|
|
|
19
35
|
declare function clearDefaultIndex(): void;
|
package/dist/data.d.ts
CHANGED
|
@@ -14,6 +14,22 @@ type TrigramIndex = {
|
|
|
14
14
|
map: Map<string, Set<number>>;
|
|
15
15
|
records: ThaiAddressRecord[];
|
|
16
16
|
zipIndex: Map<string, number[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Normalized Thai tambon names, parallel to `records` (same index).
|
|
19
|
+
* Used by the search ranker for exact/prefix match detection without
|
|
20
|
+
* re-normalizing on every query.
|
|
21
|
+
*/
|
|
22
|
+
normTambon: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Normalized English (RTGS) tambon names, parallel to `records`. Needed so
|
|
25
|
+
* romanized queries get the same own-name-beats-parent-name ranking that
|
|
26
|
+
* Thai queries get.
|
|
27
|
+
*/
|
|
28
|
+
normTambonEn: string[];
|
|
29
|
+
/** Record indices grouped by `provinceId`, in insertion order. */
|
|
30
|
+
byProvince: Map<number, number[]>;
|
|
31
|
+
/** Record indices grouped by `amphureId`, in insertion order. */
|
|
32
|
+
byAmphure: Map<number, number[]>;
|
|
17
33
|
};
|
|
18
34
|
|
|
19
35
|
declare function clearDefaultIndex(): void;
|