thaizip 0.7.5 → 0.7.7
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 +25 -145
- package/dist/data.cjs +101 -21
- package/dist/data.js +101 -21
- package/dist/index.cjs +66 -22
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +66 -22
- package/dist/react.cjs +1 -3
- package/dist/react.d.cts +1 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +1 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,176 +1,56 @@
|
|
|
1
1
|
# thaizip
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/thaizip)
|
|
4
|
-
[](https://www.npmjs.com/package/thaizip)
|
|
5
4
|
[](https://github.com/naay99999/thai-zip/blob/main/LICENSE)
|
|
6
5
|
|
|
7
|
-
Fast fuzzy autocomplete for Thai addresses — subdistrict, district, province, postal code.
|
|
6
|
+
Fast fuzzy autocomplete for Thai addresses — subdistrict, district, province, postal code.
|
|
8
7
|
|
|
9
|
-
**
|
|
8
|
+
📚 **[Documentation](https://naay99999.github.io/thai-zip/)** (ไทย / [English](https://naay99999.github.io/thai-zip/en/)) · [Playground](https://naay99999.github.io/thai-zip/en/playground/)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Fuzzy search** in Thai or English — typos and common spellings like `ladprao` still match
|
|
15
|
+
- **Postal code lookup** — exact or prefix (`10230`, `102`)
|
|
16
|
+
- **Cascade dropdowns** — province → district → subdistrict
|
|
17
|
+
- **Bundled data** — 77 provinces, 918 districts, 7,385 subdistricts, loaded lazily
|
|
18
|
+
- **Zero runtime dependencies**, works anywhere JS runs; optional React hook
|
|
19
|
+
|
|
20
|
+
## Install
|
|
14
21
|
|
|
15
22
|
```bash
|
|
16
23
|
npm install thaizip
|
|
17
24
|
```
|
|
18
25
|
|
|
19
|
-
| import path | contents |
|
|
20
|
-
|---|---|
|
|
21
|
-
| `thaizip` | core functions + types (no React code, 4.4 KB gzip) |
|
|
22
|
-
| `thaizip/react` | `useThaiAddressAutocomplete` (ships `"use client"`) |
|
|
23
|
-
| `thaizip/data` | `loadDefaultIndex`, `clearDefaultIndex`, `getDefaultIndexIfLoaded` (2.3 KB gzip, plus a 114 KB gzip data chunk fetched lazily on the first `loadDefaultIndex()` call) |
|
|
24
|
-
|
|
25
26
|
## Quick start
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
import { loadDefaultIndex } from 'thaizip/data'
|
|
29
|
-
import { searchThaiAddress,
|
|
30
|
+
import { searchThaiAddress, resolveThaiAddress } from 'thaizip'
|
|
30
31
|
|
|
31
|
-
const index = await loadDefaultIndex()
|
|
32
|
+
const index = await loadDefaultIndex()
|
|
32
33
|
|
|
33
|
-
searchThaiAddress(index, 'ลาดพร้าว')
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
formatThaiAddressSuggestion(record) // for the dropdown
|
|
38
|
-
formatThaiAddressSuggestion(record, { locale: 'en' })
|
|
39
|
-
resolveThaiAddress(record) // for saving
|
|
34
|
+
const [record] = searchThaiAddress(index, 'ลาดพร้าว')
|
|
35
|
+
resolveThaiAddress(record)
|
|
36
|
+
// { subdistrict: 'ลาดพร้าว', district: 'เขตลาดพร้าว', province: 'กรุงเทพมหานคร', postalCode: '10230', ... }
|
|
40
37
|
```
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Search
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
searchThaiAddress(index, query, {
|
|
48
|
-
limit: 10, // text queries only
|
|
49
|
-
threshold: 0.4, // match quality 0–1
|
|
50
|
-
zipLimit: Infinity, // all-digit queries — unlimited by default
|
|
51
|
-
romanizationAliases: true, // expand non-RTGS spellings
|
|
52
|
-
})
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
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 เขตลาดพร้าว.
|
|
56
|
-
|
|
57
|
-
> Combined text + zip in one query (`"ลาดพร้าว 10900"`) is not supported — search by name or zip separately.
|
|
58
|
-
|
|
59
|
-
### Postal codes
|
|
60
|
-
|
|
61
|
-
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`.
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
import { lookupByZipCode } from 'thaizip'
|
|
65
|
-
|
|
66
|
-
lookupByZipCode(index, '45000') // exact
|
|
67
|
-
lookupByZipCode(index, '450') // prefix
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Direct ZIP lookup rejects raw inputs longer than 1000 characters before scanning them. A finite `zipLimit` also bounds how many matching records are collected.
|
|
71
|
-
|
|
72
|
-
### English input
|
|
73
|
-
|
|
74
|
-
The dataset indexes official RTGS spellings, so `bang rak` and `chatuchak` work directly. 87 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`.
|
|
75
|
-
|
|
76
|
-
### Cascade dropdowns
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { listProvinces, listAmphures, listTambons } from 'thaizip'
|
|
80
|
-
|
|
81
|
-
listProvinces(index) // 77, sorted by Thai name
|
|
82
|
-
listAmphures(index, provinceId)
|
|
83
|
-
listTambons(index, amphureId) // each with its zipCode
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Backed by pre-built groupings, not scans over all 7,385 records. Unknown ids return `[]`.
|
|
87
|
-
|
|
88
|
-
## React
|
|
39
|
+
React:
|
|
89
40
|
|
|
90
41
|
```tsx
|
|
91
42
|
import { useThaiAddressAutocomplete } from 'thaizip/react'
|
|
92
43
|
|
|
93
|
-
const { query, setQuery,
|
|
94
|
-
useThaiAddressAutocomplete({ index })
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Options: `index`, `limit`, `debounce` (200ms), `threshold`, `zipLimit`, `locale`, `initialQuery`, `onSelect`.
|
|
98
|
-
|
|
99
|
-
| value | description |
|
|
100
|
-
|---|---|
|
|
101
|
-
| `query` / `setQuery` | input value; `setQuery` searches |
|
|
102
|
-
| `setQuerySilent` | set the value **without** searching or reopening the dropdown |
|
|
103
|
-
| `suggestions` | `ThaiAddressSuggestion[]` |
|
|
104
|
-
| `isOpen` | `query` is non-empty and suggestions exist |
|
|
105
|
-
| `selectSuggestion` | `(item) => ResolvedThaiAddress \| null` — clears suggestions, leaves `query` alone; `null` if the item is stale |
|
|
106
|
-
| `clear` | reset query and suggestions |
|
|
107
|
-
|
|
108
|
-
### Controlled forms
|
|
109
|
-
|
|
110
|
-
`selectSuggestion` leaves `query` untouched by design. To echo the choice back into the input use `setQuerySilent` — plain `setQuery` would immediately reopen the dropdown:
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
const handlePick = (s: ThaiAddressSuggestion) => {
|
|
114
|
-
const address = selectSuggestion(s)
|
|
115
|
-
if (!address) return
|
|
116
|
-
setQuerySilent(s.label)
|
|
117
|
-
form.setValue('address', address)
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Editing a saved address? Pass `initialQuery` rather than calling `setQuery` in an effect.
|
|
122
|
-
|
|
123
|
-
### Accessibility
|
|
124
|
-
|
|
125
|
-
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.
|
|
126
|
-
|
|
127
|
-
## Custom data
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
import { buildThaiAddressIndex, validateRawData } from 'thaizip'
|
|
131
|
-
|
|
132
|
-
const index = buildThaiAddressIndex({ provinces, amphures, tambons }, {
|
|
133
|
-
onSkip: (tambon) => console.warn('skipped:', tambon.id),
|
|
134
|
-
validate: true, // default
|
|
135
|
-
})
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
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.
|
|
139
|
-
|
|
140
|
-
`RawData` is meant to be trusted, application-controlled input — your own dataset, not arbitrary end-user uploads. `buildThaiAddressIndex` has no built-in cap on row count or field length; build cost scales linearly with total input size, but an unbounded or maliciously oversized payload (e.g. an admin importer fed directly to this function) can still cost real time and memory with nothing to stop it. If you ever build an index from untrusted input, enforce your own size limits (row count, field length) before calling `buildThaiAddressIndex`.
|
|
141
|
-
|
|
142
|
-
Types: `RawData`, `RawProvince`, `RawAmphure`, `RawTambon`.
|
|
143
|
-
|
|
144
|
-
## Types
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
type ThaiAddressSuggestion = {
|
|
148
|
-
id: string
|
|
149
|
-
label: string // follows `locale`, Thai by default
|
|
150
|
-
labelTh: string
|
|
151
|
-
labelEn: string
|
|
152
|
-
tambon: string; tambonEn: string
|
|
153
|
-
amphure: string; amphureEn: string
|
|
154
|
-
province: string; provinceEn: string
|
|
155
|
-
zipCode: string
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
type ResolvedThaiAddress = {
|
|
159
|
-
tambon: string; tambonEn: string // alias: subdistrict / subdistrictEn
|
|
160
|
-
amphure: string; amphureEn: string // alias: district / districtEn
|
|
161
|
-
province: string; provinceEn: string
|
|
162
|
-
zipCode: string // alias: postalCode
|
|
163
|
-
subdistrict: string; subdistrictEn: string
|
|
164
|
-
district: string; districtEn: string
|
|
165
|
-
postalCode: string
|
|
166
|
-
}
|
|
44
|
+
const { query, setQuery, suggestions, selectSuggestion } = useThaiAddressAutocomplete({ index })
|
|
167
45
|
```
|
|
168
46
|
|
|
169
|
-
|
|
47
|
+
Need a ready-made UI component? [`react-thaizip`](https://github.com/naay99999/react-thai-zip) scaffolds one into your project.
|
|
170
48
|
|
|
171
|
-
|
|
49
|
+
## Learn more
|
|
172
50
|
|
|
173
|
-
|
|
51
|
+
- [Getting started](https://naay99999.github.io/thai-zip/en/getting-started/)
|
|
52
|
+
- [Guides](https://naay99999.github.io/thai-zip/en/guides/search/) — search, postal codes, cascades, English input, React, custom data
|
|
53
|
+
- [API reference](https://naay99999.github.io/thai-zip/en/reference/search/)
|
|
174
54
|
|
|
175
55
|
## License
|
|
176
56
|
|
package/dist/data.cjs
CHANGED
|
@@ -35,9 +35,19 @@ function extractTrigramsNormalized(text) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// src/core/indexer.ts
|
|
38
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
39
|
+
var MAX_NAME_LENGTH = 256;
|
|
40
|
+
var MAX_ZIP_LENGTH = 32;
|
|
41
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
38
42
|
function typeErr(table, id, field, expected, value) {
|
|
39
43
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
40
44
|
}
|
|
45
|
+
function assertId(table, id, field, value) {
|
|
46
|
+
if (typeof value !== "number") throw typeErr(table, id, field, "number", value);
|
|
47
|
+
if (!Number.isSafeInteger(value)) {
|
|
48
|
+
throw new TypeError(`[thaizip] ${table} ${id}: expected safe integer for ${field}, got ${value}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
function typeName(value) {
|
|
42
52
|
return value === null ? "null" : typeof value;
|
|
43
53
|
}
|
|
@@ -55,47 +65,79 @@ function validateRawData(data) {
|
|
|
55
65
|
if (!Array.isArray(tambons)) {
|
|
56
66
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
57
67
|
}
|
|
58
|
-
|
|
68
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
69
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
70
|
+
}
|
|
71
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
59
72
|
for (let i = 0; i < provinces.length; i++) {
|
|
60
73
|
const p = provinces[i];
|
|
61
74
|
if (p === null || typeof p !== "object") {
|
|
62
75
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
63
76
|
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
77
|
+
assertId("province", p.id, "id", p.id);
|
|
78
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
79
|
+
provinceById.set(p.id, p);
|
|
67
80
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
68
81
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
82
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
83
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
69
84
|
}
|
|
70
|
-
const
|
|
85
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
71
86
|
for (let i = 0; i < amphures.length; i++) {
|
|
72
87
|
const a = amphures[i];
|
|
73
88
|
if (a === null || typeof a !== "object") {
|
|
74
89
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
75
90
|
}
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
91
|
+
assertId("amphure", a.id, "id", a.id);
|
|
92
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
93
|
+
amphureById.set(a.id, a);
|
|
79
94
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
80
95
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
81
|
-
|
|
96
|
+
assertId("amphure", a.id, "province_id", a.province_id);
|
|
97
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
98
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
82
99
|
}
|
|
83
100
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
101
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
102
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
103
|
+
let postingBudget = 0;
|
|
84
104
|
for (let i = 0; i < tambons.length; i++) {
|
|
85
105
|
const t = tambons[i];
|
|
86
106
|
if (t === null || typeof t !== "object") {
|
|
87
107
|
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
88
108
|
}
|
|
89
|
-
|
|
109
|
+
assertId("tambon", t.id, "id", t.id);
|
|
90
110
|
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
91
111
|
seenTambonIds.add(t.id);
|
|
92
112
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
93
113
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
114
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
115
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
94
116
|
const zipType = typeof t.zip_code;
|
|
95
117
|
if (zipType !== "string" && zipType !== "number") {
|
|
96
118
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
97
119
|
}
|
|
98
|
-
|
|
120
|
+
assertId("tambon", t.id, "amphure_id", t.amphure_id);
|
|
121
|
+
const zipLength = String(t.zip_code).length;
|
|
122
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
123
|
+
if (t.deleted_at) continue;
|
|
124
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
125
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
126
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
127
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
128
|
+
if (provinceGrams === void 0) {
|
|
129
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
130
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
131
|
+
}
|
|
132
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
133
|
+
if (amphureGrams === void 0) {
|
|
134
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
135
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
136
|
+
}
|
|
137
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
138
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
139
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
140
|
+
}
|
|
99
141
|
}
|
|
100
142
|
}
|
|
101
143
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -126,13 +168,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
126
168
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
127
169
|
);
|
|
128
170
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
129
|
-
for (const [id, prov] of provMap) {
|
|
130
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
131
|
-
}
|
|
132
171
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
133
|
-
for (const [id, amp] of ampMap) {
|
|
134
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
135
|
-
}
|
|
136
172
|
const records = [];
|
|
137
173
|
const map = /* @__PURE__ */ new Map();
|
|
138
174
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -182,8 +218,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
182
218
|
normTambonEn.push(normEn);
|
|
183
219
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
184
220
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
222
|
+
if (!provinceGrams) {
|
|
223
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
224
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
225
|
+
}
|
|
226
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
227
|
+
if (!amphureGrams) {
|
|
228
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
229
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
230
|
+
}
|
|
231
|
+
addTrigrams(map, provinceGrams, idx);
|
|
232
|
+
addTrigrams(map, amphureGrams, idx);
|
|
187
233
|
}
|
|
188
234
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
189
235
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
@@ -194,6 +240,40 @@ function buildThaiAddressIndex(data, options) {
|
|
|
194
240
|
var cached = null;
|
|
195
241
|
var inflightPromise = null;
|
|
196
242
|
var loadGeneration = 0;
|
|
243
|
+
function rejectDefaultIndexMutation() {
|
|
244
|
+
throw new TypeError("[thaizip] the shared default index is read-only");
|
|
245
|
+
}
|
|
246
|
+
var readOnlyMapPrototype = Object.create(Map.prototype);
|
|
247
|
+
var readOnlySetPrototype = Object.create(Set.prototype);
|
|
248
|
+
for (const [prototype, methods] of [
|
|
249
|
+
[readOnlyMapPrototype, ["set", "delete", "clear"]],
|
|
250
|
+
[readOnlySetPrototype, ["add", "delete", "clear"]]
|
|
251
|
+
]) {
|
|
252
|
+
for (const method of methods) {
|
|
253
|
+
Object.defineProperty(prototype, method, { value: rejectDefaultIndexMutation });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function lockMapValues(map, lockValue) {
|
|
257
|
+
for (const value of map.values()) lockValue(value);
|
|
258
|
+
Object.setPrototypeOf(map, readOnlyMapPrototype);
|
|
259
|
+
Object.freeze(map);
|
|
260
|
+
}
|
|
261
|
+
function lockDefaultIndex(index) {
|
|
262
|
+
for (const record of index.records) Object.freeze(record);
|
|
263
|
+
Object.freeze(index.records);
|
|
264
|
+
Object.freeze(index.normTambon);
|
|
265
|
+
Object.freeze(index.normTambonEn);
|
|
266
|
+
if (index.sortedZipKeys) Object.freeze(index.sortedZipKeys);
|
|
267
|
+
if (index.sortedZipPostings) Object.freeze(index.sortedZipPostings);
|
|
268
|
+
lockMapValues(index.map, (set) => {
|
|
269
|
+
Object.setPrototypeOf(set, readOnlySetPrototype);
|
|
270
|
+
Object.freeze(set);
|
|
271
|
+
});
|
|
272
|
+
lockMapValues(index.zipIndex, Object.freeze);
|
|
273
|
+
lockMapValues(index.byProvince, Object.freeze);
|
|
274
|
+
lockMapValues(index.byAmphure, Object.freeze);
|
|
275
|
+
return Object.freeze(index);
|
|
276
|
+
}
|
|
197
277
|
function clearDefaultIndex() {
|
|
198
278
|
cached = null;
|
|
199
279
|
inflightPromise = null;
|
|
@@ -207,11 +287,11 @@ async function loadDefaultIndex() {
|
|
|
207
287
|
if (!inflightPromise) {
|
|
208
288
|
const gen = loadGeneration;
|
|
209
289
|
inflightPromise = import('./defaultData.js').then(({ p, a, t }) => {
|
|
210
|
-
const index = buildThaiAddressIndex({
|
|
290
|
+
const index = lockDefaultIndex(buildThaiAddressIndex({
|
|
211
291
|
provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
|
|
212
292
|
amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
|
|
213
293
|
tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
|
|
214
|
-
}, { validate: false });
|
|
294
|
+
}, { validate: false }));
|
|
215
295
|
if (loadGeneration === gen) {
|
|
216
296
|
cached = index;
|
|
217
297
|
inflightPromise = null;
|
package/dist/data.js
CHANGED
|
@@ -33,9 +33,19 @@ function extractTrigramsNormalized(text) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/core/indexer.ts
|
|
36
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
37
|
+
var MAX_NAME_LENGTH = 256;
|
|
38
|
+
var MAX_ZIP_LENGTH = 32;
|
|
39
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
36
40
|
function typeErr(table, id, field, expected, value) {
|
|
37
41
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
38
42
|
}
|
|
43
|
+
function assertId(table, id, field, value) {
|
|
44
|
+
if (typeof value !== "number") throw typeErr(table, id, field, "number", value);
|
|
45
|
+
if (!Number.isSafeInteger(value)) {
|
|
46
|
+
throw new TypeError(`[thaizip] ${table} ${id}: expected safe integer for ${field}, got ${value}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
39
49
|
function typeName(value) {
|
|
40
50
|
return value === null ? "null" : typeof value;
|
|
41
51
|
}
|
|
@@ -53,47 +63,79 @@ function validateRawData(data) {
|
|
|
53
63
|
if (!Array.isArray(tambons)) {
|
|
54
64
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
55
65
|
}
|
|
56
|
-
|
|
66
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
67
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
68
|
+
}
|
|
69
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
57
70
|
for (let i = 0; i < provinces.length; i++) {
|
|
58
71
|
const p = provinces[i];
|
|
59
72
|
if (p === null || typeof p !== "object") {
|
|
60
73
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
61
74
|
}
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
75
|
+
assertId("province", p.id, "id", p.id);
|
|
76
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
77
|
+
provinceById.set(p.id, p);
|
|
65
78
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
66
79
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
80
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
81
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
67
82
|
}
|
|
68
|
-
const
|
|
83
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
69
84
|
for (let i = 0; i < amphures.length; i++) {
|
|
70
85
|
const a = amphures[i];
|
|
71
86
|
if (a === null || typeof a !== "object") {
|
|
72
87
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
73
88
|
}
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
89
|
+
assertId("amphure", a.id, "id", a.id);
|
|
90
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
91
|
+
amphureById.set(a.id, a);
|
|
77
92
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
78
93
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
79
|
-
|
|
94
|
+
assertId("amphure", a.id, "province_id", a.province_id);
|
|
95
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
96
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
80
97
|
}
|
|
81
98
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
99
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
100
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
101
|
+
let postingBudget = 0;
|
|
82
102
|
for (let i = 0; i < tambons.length; i++) {
|
|
83
103
|
const t = tambons[i];
|
|
84
104
|
if (t === null || typeof t !== "object") {
|
|
85
105
|
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
86
106
|
}
|
|
87
|
-
|
|
107
|
+
assertId("tambon", t.id, "id", t.id);
|
|
88
108
|
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
89
109
|
seenTambonIds.add(t.id);
|
|
90
110
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
91
111
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
112
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
113
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
92
114
|
const zipType = typeof t.zip_code;
|
|
93
115
|
if (zipType !== "string" && zipType !== "number") {
|
|
94
116
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
95
117
|
}
|
|
96
|
-
|
|
118
|
+
assertId("tambon", t.id, "amphure_id", t.amphure_id);
|
|
119
|
+
const zipLength = String(t.zip_code).length;
|
|
120
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
121
|
+
if (t.deleted_at) continue;
|
|
122
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
123
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
124
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
125
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
126
|
+
if (provinceGrams === void 0) {
|
|
127
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
128
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
129
|
+
}
|
|
130
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
131
|
+
if (amphureGrams === void 0) {
|
|
132
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
133
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
134
|
+
}
|
|
135
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
136
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
137
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
138
|
+
}
|
|
97
139
|
}
|
|
98
140
|
}
|
|
99
141
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -124,13 +166,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
124
166
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
125
167
|
);
|
|
126
168
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
127
|
-
for (const [id, prov] of provMap) {
|
|
128
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
129
|
-
}
|
|
130
169
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
131
|
-
for (const [id, amp] of ampMap) {
|
|
132
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
133
|
-
}
|
|
134
170
|
const records = [];
|
|
135
171
|
const map = /* @__PURE__ */ new Map();
|
|
136
172
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -180,8 +216,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
180
216
|
normTambonEn.push(normEn);
|
|
181
217
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
182
218
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
220
|
+
if (!provinceGrams) {
|
|
221
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
222
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
223
|
+
}
|
|
224
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
225
|
+
if (!amphureGrams) {
|
|
226
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
227
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
228
|
+
}
|
|
229
|
+
addTrigrams(map, provinceGrams, idx);
|
|
230
|
+
addTrigrams(map, amphureGrams, idx);
|
|
185
231
|
}
|
|
186
232
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
187
233
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
@@ -192,6 +238,40 @@ function buildThaiAddressIndex(data, options) {
|
|
|
192
238
|
var cached = null;
|
|
193
239
|
var inflightPromise = null;
|
|
194
240
|
var loadGeneration = 0;
|
|
241
|
+
function rejectDefaultIndexMutation() {
|
|
242
|
+
throw new TypeError("[thaizip] the shared default index is read-only");
|
|
243
|
+
}
|
|
244
|
+
var readOnlyMapPrototype = Object.create(Map.prototype);
|
|
245
|
+
var readOnlySetPrototype = Object.create(Set.prototype);
|
|
246
|
+
for (const [prototype, methods] of [
|
|
247
|
+
[readOnlyMapPrototype, ["set", "delete", "clear"]],
|
|
248
|
+
[readOnlySetPrototype, ["add", "delete", "clear"]]
|
|
249
|
+
]) {
|
|
250
|
+
for (const method of methods) {
|
|
251
|
+
Object.defineProperty(prototype, method, { value: rejectDefaultIndexMutation });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function lockMapValues(map, lockValue) {
|
|
255
|
+
for (const value of map.values()) lockValue(value);
|
|
256
|
+
Object.setPrototypeOf(map, readOnlyMapPrototype);
|
|
257
|
+
Object.freeze(map);
|
|
258
|
+
}
|
|
259
|
+
function lockDefaultIndex(index) {
|
|
260
|
+
for (const record of index.records) Object.freeze(record);
|
|
261
|
+
Object.freeze(index.records);
|
|
262
|
+
Object.freeze(index.normTambon);
|
|
263
|
+
Object.freeze(index.normTambonEn);
|
|
264
|
+
if (index.sortedZipKeys) Object.freeze(index.sortedZipKeys);
|
|
265
|
+
if (index.sortedZipPostings) Object.freeze(index.sortedZipPostings);
|
|
266
|
+
lockMapValues(index.map, (set) => {
|
|
267
|
+
Object.setPrototypeOf(set, readOnlySetPrototype);
|
|
268
|
+
Object.freeze(set);
|
|
269
|
+
});
|
|
270
|
+
lockMapValues(index.zipIndex, Object.freeze);
|
|
271
|
+
lockMapValues(index.byProvince, Object.freeze);
|
|
272
|
+
lockMapValues(index.byAmphure, Object.freeze);
|
|
273
|
+
return Object.freeze(index);
|
|
274
|
+
}
|
|
195
275
|
function clearDefaultIndex() {
|
|
196
276
|
cached = null;
|
|
197
277
|
inflightPromise = null;
|
|
@@ -205,11 +285,11 @@ async function loadDefaultIndex() {
|
|
|
205
285
|
if (!inflightPromise) {
|
|
206
286
|
const gen = loadGeneration;
|
|
207
287
|
inflightPromise = import('./defaultData.js').then(({ p, a, t }) => {
|
|
208
|
-
const index = buildThaiAddressIndex({
|
|
288
|
+
const index = lockDefaultIndex(buildThaiAddressIndex({
|
|
209
289
|
provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
|
|
210
290
|
amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
|
|
211
291
|
tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
|
|
212
|
-
}, { validate: false });
|
|
292
|
+
}, { validate: false }));
|
|
213
293
|
if (loadGeneration === gen) {
|
|
214
294
|
cached = index;
|
|
215
295
|
inflightPromise = null;
|
package/dist/index.cjs
CHANGED
|
@@ -35,9 +35,19 @@ function extractTrigramsNormalized(text) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// src/core/indexer.ts
|
|
38
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
39
|
+
var MAX_NAME_LENGTH = 256;
|
|
40
|
+
var MAX_ZIP_LENGTH = 32;
|
|
41
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
38
42
|
function typeErr(table, id, field, expected, value) {
|
|
39
43
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
40
44
|
}
|
|
45
|
+
function assertId(table, id, field, value) {
|
|
46
|
+
if (typeof value !== "number") throw typeErr(table, id, field, "number", value);
|
|
47
|
+
if (!Number.isSafeInteger(value)) {
|
|
48
|
+
throw new TypeError(`[thaizip] ${table} ${id}: expected safe integer for ${field}, got ${value}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
function typeName(value) {
|
|
42
52
|
return value === null ? "null" : typeof value;
|
|
43
53
|
}
|
|
@@ -55,47 +65,79 @@ function validateRawData(data) {
|
|
|
55
65
|
if (!Array.isArray(tambons)) {
|
|
56
66
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
57
67
|
}
|
|
58
|
-
|
|
68
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
69
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
70
|
+
}
|
|
71
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
59
72
|
for (let i = 0; i < provinces.length; i++) {
|
|
60
73
|
const p = provinces[i];
|
|
61
74
|
if (p === null || typeof p !== "object") {
|
|
62
75
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
63
76
|
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
77
|
+
assertId("province", p.id, "id", p.id);
|
|
78
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
79
|
+
provinceById.set(p.id, p);
|
|
67
80
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
68
81
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
82
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
83
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
69
84
|
}
|
|
70
|
-
const
|
|
85
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
71
86
|
for (let i = 0; i < amphures.length; i++) {
|
|
72
87
|
const a = amphures[i];
|
|
73
88
|
if (a === null || typeof a !== "object") {
|
|
74
89
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
75
90
|
}
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
91
|
+
assertId("amphure", a.id, "id", a.id);
|
|
92
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
93
|
+
amphureById.set(a.id, a);
|
|
79
94
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
80
95
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
81
|
-
|
|
96
|
+
assertId("amphure", a.id, "province_id", a.province_id);
|
|
97
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
98
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
82
99
|
}
|
|
83
100
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
101
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
102
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
103
|
+
let postingBudget = 0;
|
|
84
104
|
for (let i = 0; i < tambons.length; i++) {
|
|
85
105
|
const t = tambons[i];
|
|
86
106
|
if (t === null || typeof t !== "object") {
|
|
87
107
|
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
88
108
|
}
|
|
89
|
-
|
|
109
|
+
assertId("tambon", t.id, "id", t.id);
|
|
90
110
|
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
91
111
|
seenTambonIds.add(t.id);
|
|
92
112
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
93
113
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
114
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
115
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
94
116
|
const zipType = typeof t.zip_code;
|
|
95
117
|
if (zipType !== "string" && zipType !== "number") {
|
|
96
118
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
97
119
|
}
|
|
98
|
-
|
|
120
|
+
assertId("tambon", t.id, "amphure_id", t.amphure_id);
|
|
121
|
+
const zipLength = String(t.zip_code).length;
|
|
122
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
123
|
+
if (t.deleted_at) continue;
|
|
124
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
125
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
126
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
127
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
128
|
+
if (provinceGrams === void 0) {
|
|
129
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
130
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
131
|
+
}
|
|
132
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
133
|
+
if (amphureGrams === void 0) {
|
|
134
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
135
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
136
|
+
}
|
|
137
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
138
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
139
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
140
|
+
}
|
|
99
141
|
}
|
|
100
142
|
}
|
|
101
143
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -126,13 +168,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
126
168
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
127
169
|
);
|
|
128
170
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
129
|
-
for (const [id, prov] of provMap) {
|
|
130
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
131
|
-
}
|
|
132
171
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
133
|
-
for (const [id, amp] of ampMap) {
|
|
134
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
135
|
-
}
|
|
136
172
|
const records = [];
|
|
137
173
|
const map = /* @__PURE__ */ new Map();
|
|
138
174
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -182,8 +218,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
182
218
|
normTambonEn.push(normEn);
|
|
183
219
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
184
220
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
222
|
+
if (!provinceGrams) {
|
|
223
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
224
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
225
|
+
}
|
|
226
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
227
|
+
if (!amphureGrams) {
|
|
228
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
229
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
230
|
+
}
|
|
231
|
+
addTrigrams(map, provinceGrams, idx);
|
|
232
|
+
addTrigrams(map, amphureGrams, idx);
|
|
187
233
|
}
|
|
188
234
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
189
235
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
@@ -381,7 +427,7 @@ function searchThaiAddress(index, query, options) {
|
|
|
381
427
|
if (ZIP_CODE_RE.test(normalized)) {
|
|
382
428
|
if (normalized.length < 2) return [];
|
|
383
429
|
return lookupByZipCode(index, normalized, {
|
|
384
|
-
zipLimit: options?.zipLimit ?? Infinity
|
|
430
|
+
zipLimit: options?.zipLimit ?? (normalized.length >= 5 ? Infinity : limit)
|
|
385
431
|
});
|
|
386
432
|
}
|
|
387
433
|
if (normalized.length < 3) return [];
|
|
@@ -543,5 +589,3 @@ exports.normalizeThaiAddressText = normalizeThaiAddressText;
|
|
|
543
589
|
exports.resolveThaiAddress = resolveThaiAddress;
|
|
544
590
|
exports.searchThaiAddress = searchThaiAddress;
|
|
545
591
|
exports.validateRawData = validateRawData;
|
|
546
|
-
//# sourceMappingURL=index.cjs.map
|
|
547
|
-
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.cts
CHANGED
|
@@ -101,9 +101,11 @@ type SearchOptions = {
|
|
|
101
101
|
limit?: number;
|
|
102
102
|
threshold?: number;
|
|
103
103
|
/**
|
|
104
|
-
* Maximum results for all-digit (zip code) queries.
|
|
105
|
-
*
|
|
106
|
-
* the text-autocomplete `limit` silently
|
|
104
|
+
* Maximum results for all-digit (zip code) queries. When omitted, a complete
|
|
105
|
+
* 5-digit zip returns every matching tambon (at most a few dozen — capping
|
|
106
|
+
* those at the text-autocomplete `limit` would silently hide valid
|
|
107
|
+
* subdistricts), while a partial prefix (2–4 digits, which can match hundreds
|
|
108
|
+
* of records) is capped at `limit`. Pass `Infinity` to return every prefix match.
|
|
107
109
|
*/
|
|
108
110
|
zipLimit?: number;
|
|
109
111
|
/**
|
|
@@ -145,6 +147,10 @@ type BuildIndexOptions = {
|
|
|
145
147
|
* Validate that every consumed field has the expected runtime type and throw a
|
|
146
148
|
* descriptive `TypeError` if not. Defaults to `true`; set to `false` to skip
|
|
147
149
|
* the checks when the data source is already trusted (e.g. the bundled index).
|
|
150
|
+
*
|
|
151
|
+
* `false` also skips the row, name-length and posting-budget limits that bound
|
|
152
|
+
* memory and CPU use, so never disable it for data you do not control
|
|
153
|
+
* (user uploads, third-party APIs, a CMS editable by others).
|
|
148
154
|
*/
|
|
149
155
|
validate?: boolean;
|
|
150
156
|
};
|
|
@@ -161,10 +167,9 @@ type RawData = {
|
|
|
161
167
|
* private datasets) who want to fail fast with a descriptive error instead of an
|
|
162
168
|
* opaque crash deep inside the normalizer, or silent `"undefined"` labels.
|
|
163
169
|
*
|
|
164
|
-
* Throws a `TypeError`
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* provided.
|
|
170
|
+
* Throws a `TypeError` for bad shapes or duplicate IDs and a `RangeError` for
|
|
171
|
+
* resource limits (all prefixed `[thaizip]`). Does not filter soft-deleted rows
|
|
172
|
+
* before shape checks — validates every row as provided.
|
|
168
173
|
*
|
|
169
174
|
* IDs must be unique within each table (duplicates throw, they are not silently
|
|
170
175
|
* discarded — two suggestions sharing an id would resolve to the wrong record).
|
package/dist/index.d.ts
CHANGED
|
@@ -101,9 +101,11 @@ type SearchOptions = {
|
|
|
101
101
|
limit?: number;
|
|
102
102
|
threshold?: number;
|
|
103
103
|
/**
|
|
104
|
-
* Maximum results for all-digit (zip code) queries.
|
|
105
|
-
*
|
|
106
|
-
* the text-autocomplete `limit` silently
|
|
104
|
+
* Maximum results for all-digit (zip code) queries. When omitted, a complete
|
|
105
|
+
* 5-digit zip returns every matching tambon (at most a few dozen — capping
|
|
106
|
+
* those at the text-autocomplete `limit` would silently hide valid
|
|
107
|
+
* subdistricts), while a partial prefix (2–4 digits, which can match hundreds
|
|
108
|
+
* of records) is capped at `limit`. Pass `Infinity` to return every prefix match.
|
|
107
109
|
*/
|
|
108
110
|
zipLimit?: number;
|
|
109
111
|
/**
|
|
@@ -145,6 +147,10 @@ type BuildIndexOptions = {
|
|
|
145
147
|
* Validate that every consumed field has the expected runtime type and throw a
|
|
146
148
|
* descriptive `TypeError` if not. Defaults to `true`; set to `false` to skip
|
|
147
149
|
* the checks when the data source is already trusted (e.g. the bundled index).
|
|
150
|
+
*
|
|
151
|
+
* `false` also skips the row, name-length and posting-budget limits that bound
|
|
152
|
+
* memory and CPU use, so never disable it for data you do not control
|
|
153
|
+
* (user uploads, third-party APIs, a CMS editable by others).
|
|
148
154
|
*/
|
|
149
155
|
validate?: boolean;
|
|
150
156
|
};
|
|
@@ -161,10 +167,9 @@ type RawData = {
|
|
|
161
167
|
* private datasets) who want to fail fast with a descriptive error instead of an
|
|
162
168
|
* opaque crash deep inside the normalizer, or silent `"undefined"` labels.
|
|
163
169
|
*
|
|
164
|
-
* Throws a `TypeError`
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* provided.
|
|
170
|
+
* Throws a `TypeError` for bad shapes or duplicate IDs and a `RangeError` for
|
|
171
|
+
* resource limits (all prefixed `[thaizip]`). Does not filter soft-deleted rows
|
|
172
|
+
* before shape checks — validates every row as provided.
|
|
168
173
|
*
|
|
169
174
|
* IDs must be unique within each table (duplicates throw, they are not silently
|
|
170
175
|
* discarded — two suggestions sharing an id would resolve to the wrong record).
|
package/dist/index.js
CHANGED
|
@@ -33,9 +33,19 @@ function extractTrigramsNormalized(text) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/core/indexer.ts
|
|
36
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
37
|
+
var MAX_NAME_LENGTH = 256;
|
|
38
|
+
var MAX_ZIP_LENGTH = 32;
|
|
39
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
36
40
|
function typeErr(table, id, field, expected, value) {
|
|
37
41
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
38
42
|
}
|
|
43
|
+
function assertId(table, id, field, value) {
|
|
44
|
+
if (typeof value !== "number") throw typeErr(table, id, field, "number", value);
|
|
45
|
+
if (!Number.isSafeInteger(value)) {
|
|
46
|
+
throw new TypeError(`[thaizip] ${table} ${id}: expected safe integer for ${field}, got ${value}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
39
49
|
function typeName(value) {
|
|
40
50
|
return value === null ? "null" : typeof value;
|
|
41
51
|
}
|
|
@@ -53,47 +63,79 @@ function validateRawData(data) {
|
|
|
53
63
|
if (!Array.isArray(tambons)) {
|
|
54
64
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
55
65
|
}
|
|
56
|
-
|
|
66
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
67
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
68
|
+
}
|
|
69
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
57
70
|
for (let i = 0; i < provinces.length; i++) {
|
|
58
71
|
const p = provinces[i];
|
|
59
72
|
if (p === null || typeof p !== "object") {
|
|
60
73
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
61
74
|
}
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
75
|
+
assertId("province", p.id, "id", p.id);
|
|
76
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
77
|
+
provinceById.set(p.id, p);
|
|
65
78
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
66
79
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
80
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
81
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
67
82
|
}
|
|
68
|
-
const
|
|
83
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
69
84
|
for (let i = 0; i < amphures.length; i++) {
|
|
70
85
|
const a = amphures[i];
|
|
71
86
|
if (a === null || typeof a !== "object") {
|
|
72
87
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
73
88
|
}
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
89
|
+
assertId("amphure", a.id, "id", a.id);
|
|
90
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
91
|
+
amphureById.set(a.id, a);
|
|
77
92
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
78
93
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
79
|
-
|
|
94
|
+
assertId("amphure", a.id, "province_id", a.province_id);
|
|
95
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
96
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
80
97
|
}
|
|
81
98
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
99
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
100
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
101
|
+
let postingBudget = 0;
|
|
82
102
|
for (let i = 0; i < tambons.length; i++) {
|
|
83
103
|
const t = tambons[i];
|
|
84
104
|
if (t === null || typeof t !== "object") {
|
|
85
105
|
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
86
106
|
}
|
|
87
|
-
|
|
107
|
+
assertId("tambon", t.id, "id", t.id);
|
|
88
108
|
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
89
109
|
seenTambonIds.add(t.id);
|
|
90
110
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
91
111
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
112
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
113
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
92
114
|
const zipType = typeof t.zip_code;
|
|
93
115
|
if (zipType !== "string" && zipType !== "number") {
|
|
94
116
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
95
117
|
}
|
|
96
|
-
|
|
118
|
+
assertId("tambon", t.id, "amphure_id", t.amphure_id);
|
|
119
|
+
const zipLength = String(t.zip_code).length;
|
|
120
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
121
|
+
if (t.deleted_at) continue;
|
|
122
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
123
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
124
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
125
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
126
|
+
if (provinceGrams === void 0) {
|
|
127
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
128
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
129
|
+
}
|
|
130
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
131
|
+
if (amphureGrams === void 0) {
|
|
132
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
133
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
134
|
+
}
|
|
135
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
136
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
137
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
138
|
+
}
|
|
97
139
|
}
|
|
98
140
|
}
|
|
99
141
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -124,13 +166,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
124
166
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
125
167
|
);
|
|
126
168
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
127
|
-
for (const [id, prov] of provMap) {
|
|
128
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
129
|
-
}
|
|
130
169
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
131
|
-
for (const [id, amp] of ampMap) {
|
|
132
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
133
|
-
}
|
|
134
170
|
const records = [];
|
|
135
171
|
const map = /* @__PURE__ */ new Map();
|
|
136
172
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -180,8 +216,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
180
216
|
normTambonEn.push(normEn);
|
|
181
217
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
182
218
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
220
|
+
if (!provinceGrams) {
|
|
221
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
222
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
223
|
+
}
|
|
224
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
225
|
+
if (!amphureGrams) {
|
|
226
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
227
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
228
|
+
}
|
|
229
|
+
addTrigrams(map, provinceGrams, idx);
|
|
230
|
+
addTrigrams(map, amphureGrams, idx);
|
|
185
231
|
}
|
|
186
232
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
187
233
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
@@ -379,7 +425,7 @@ function searchThaiAddress(index, query, options) {
|
|
|
379
425
|
if (ZIP_CODE_RE.test(normalized)) {
|
|
380
426
|
if (normalized.length < 2) return [];
|
|
381
427
|
return lookupByZipCode(index, normalized, {
|
|
382
|
-
zipLimit: options?.zipLimit ?? Infinity
|
|
428
|
+
zipLimit: options?.zipLimit ?? (normalized.length >= 5 ? Infinity : limit)
|
|
383
429
|
});
|
|
384
430
|
}
|
|
385
431
|
if (normalized.length < 3) return [];
|
|
@@ -531,5 +577,3 @@ function resolveThaiAddress(record) {
|
|
|
531
577
|
}
|
|
532
578
|
|
|
533
579
|
export { applyRomanizationAliases, buildThaiAddressIndex, formatThaiAddressSuggestion, listAmphures, listProvinces, listTambons, lookupByZipCode, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, validateRawData };
|
|
534
|
-
//# sourceMappingURL=index.js.map
|
|
535
|
-
//# sourceMappingURL=index.js.map
|
package/dist/react.cjs
CHANGED
|
@@ -225,7 +225,7 @@ function searchThaiAddress(index, query, options) {
|
|
|
225
225
|
if (ZIP_CODE_RE.test(normalized)) {
|
|
226
226
|
if (normalized.length < 2) return [];
|
|
227
227
|
return lookupByZipCode(index, normalized, {
|
|
228
|
-
zipLimit: options?.zipLimit ?? Infinity
|
|
228
|
+
zipLimit: options?.zipLimit ?? (normalized.length >= 5 ? Infinity : limit)
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
231
|
if (normalized.length < 3) return [];
|
|
@@ -402,5 +402,3 @@ function useThaiAddressAutocomplete(options) {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
exports.useThaiAddressAutocomplete = useThaiAddressAutocomplete;
|
|
405
|
-
//# sourceMappingURL=react.cjs.map
|
|
406
|
-
//# sourceMappingURL=react.cjs.map
|
package/dist/react.d.cts
CHANGED
|
@@ -79,6 +79,7 @@ type UseThaiAddressAutocompleteOptions = {
|
|
|
79
79
|
limit?: number;
|
|
80
80
|
debounce?: number;
|
|
81
81
|
threshold?: number;
|
|
82
|
+
/** Maximum results for zip code queries; see `SearchOptions.zipLimit` for the default. */
|
|
82
83
|
zipLimit?: number;
|
|
83
84
|
/** Seed value for `query` on first render. Defaults to `''`. */
|
|
84
85
|
initialQuery?: string;
|
package/dist/react.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ type UseThaiAddressAutocompleteOptions = {
|
|
|
79
79
|
limit?: number;
|
|
80
80
|
debounce?: number;
|
|
81
81
|
threshold?: number;
|
|
82
|
+
/** Maximum results for zip code queries; see `SearchOptions.zipLimit` for the default. */
|
|
82
83
|
zipLimit?: number;
|
|
83
84
|
/** Seed value for `query` on first render. Defaults to `''`. */
|
|
84
85
|
initialQuery?: string;
|
package/dist/react.js
CHANGED
|
@@ -223,7 +223,7 @@ function searchThaiAddress(index, query, options) {
|
|
|
223
223
|
if (ZIP_CODE_RE.test(normalized)) {
|
|
224
224
|
if (normalized.length < 2) return [];
|
|
225
225
|
return lookupByZipCode(index, normalized, {
|
|
226
|
-
zipLimit: options?.zipLimit ?? Infinity
|
|
226
|
+
zipLimit: options?.zipLimit ?? (normalized.length >= 5 ? Infinity : limit)
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
if (normalized.length < 3) return [];
|
|
@@ -400,5 +400,3 @@ function useThaiAddressAutocomplete(options) {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
export { useThaiAddressAutocomplete };
|
|
403
|
-
//# sourceMappingURL=react.js.map
|
|
404
|
-
//# sourceMappingURL=react.js.map
|