thaizip 0.7.2 → 0.7.4
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/LICENSE +21 -0
- package/README.md +15 -1
- package/dist/data.cjs +53 -8
- package/dist/data.d.cts +9 -0
- package/dist/data.d.ts +9 -0
- package/dist/data.js +53 -8
- package/dist/index.cjs +105 -25
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +105 -25
- package/dist/react.cjs +71 -39
- package/dist/react.d.cts +9 -0
- package/dist/react.d.ts +9 -0
- package/dist/react.js +71 -39
- package/package.json +11 -5
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
package/dist/react.cjs
CHANGED
|
@@ -7,8 +7,13 @@ var react = require('react');
|
|
|
7
7
|
var TONE_MARKS = /[่-๋็์]/g;
|
|
8
8
|
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
|
|
9
9
|
function normalizeThaiAddressText(input) {
|
|
10
|
-
if (
|
|
11
|
-
|
|
10
|
+
if (typeof input !== "string") {
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
`[thaizip] normalizeThaiAddressText expected string, got ${input === null ? "null" : typeof input}`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (input.length === 0) return "";
|
|
16
|
+
let text = input.normalize("NFC").trim();
|
|
12
17
|
text = text.replace(ADDRESS_PREFIXES, "").trim();
|
|
13
18
|
text = text.replace(TONE_MARKS, "");
|
|
14
19
|
text = text.toLowerCase();
|
|
@@ -147,27 +152,44 @@ function compareZip(a, b) {
|
|
|
147
152
|
if (a > b) return 1;
|
|
148
153
|
return 0;
|
|
149
154
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return compareZip(a.zipCode, b.zipCode);
|
|
155
|
-
});
|
|
156
|
-
return matches;
|
|
155
|
+
function normalizeLimit(value) {
|
|
156
|
+
if (Number.isNaN(value) || value === -Infinity) return 0;
|
|
157
|
+
if (value === Infinity) return value;
|
|
158
|
+
return Math.max(0, Math.floor(value));
|
|
157
159
|
}
|
|
158
160
|
function lookupByZipCode(index, zip, options) {
|
|
159
|
-
if (!index || !zip) return [];
|
|
161
|
+
if (!index || !index.zipIndex || !index.records || typeof zip !== "string") return [];
|
|
162
|
+
if (zip.length > 1e3) return [];
|
|
163
|
+
const zipLimit = normalizeLimit(options?.zipLimit ?? Infinity);
|
|
164
|
+
if (zipLimit === 0) return [];
|
|
160
165
|
const normalized = zip.trim();
|
|
161
166
|
if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return [];
|
|
162
|
-
const zipLimit = options?.zipLimit ?? Infinity;
|
|
163
167
|
const matches = [];
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
const keys = index.sortedZipKeys;
|
|
169
|
+
const postings = index.sortedZipPostings;
|
|
170
|
+
if (keys !== void 0 && postings !== void 0) {
|
|
171
|
+
let lo = 0;
|
|
172
|
+
let hi = keys.length;
|
|
173
|
+
while (lo < hi) {
|
|
174
|
+
const mid = lo + hi >> 1;
|
|
175
|
+
if (keys[mid] < normalized) lo = mid + 1;
|
|
176
|
+
else hi = mid;
|
|
177
|
+
}
|
|
178
|
+
for (let i = lo; i < keys.length && keys[i].startsWith(normalized) && matches.length < zipLimit; i++) {
|
|
179
|
+
const indices = postings[i];
|
|
180
|
+
for (let j = 0; j < indices.length && matches.length < zipLimit; j++) matches.push(index.records[indices[j]]);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const matchingKeys = [...index.zipIndex.keys()].filter((z) => z.startsWith(normalized)).sort(compareZip);
|
|
184
|
+
for (const z of matchingKeys) {
|
|
185
|
+
if (matches.length >= zipLimit) break;
|
|
186
|
+
for (const idx of index.zipIndex.get(z)) {
|
|
187
|
+
if (matches.length >= zipLimit) break;
|
|
188
|
+
matches.push(index.records[idx]);
|
|
189
|
+
}
|
|
167
190
|
}
|
|
168
191
|
}
|
|
169
|
-
|
|
170
|
-
return matches.slice(0, zipLimit);
|
|
192
|
+
return matches.slice(0, zipLimit).map((record) => ({ ...record }));
|
|
171
193
|
}
|
|
172
194
|
function rankAgainstName(name, query) {
|
|
173
195
|
if (name === void 0 || name.length === 0) return 0;
|
|
@@ -182,10 +204,20 @@ function computeMatchRank(index, idx, query) {
|
|
|
182
204
|
const en = rankAgainstName(index.normTambonEn?.[idx], query);
|
|
183
205
|
return en > th ? en : th;
|
|
184
206
|
}
|
|
207
|
+
var scratchByIndex = /* @__PURE__ */ new WeakMap();
|
|
208
|
+
function getScratch(index) {
|
|
209
|
+
const n = index.records.length;
|
|
210
|
+
let s = scratchByIndex.get(index);
|
|
211
|
+
if (s === void 0 || s.counts.length < n) {
|
|
212
|
+
s = { counts: new Uint32Array(n), touched: new Int32Array(n) };
|
|
213
|
+
scratchByIndex.set(index, s);
|
|
214
|
+
}
|
|
215
|
+
return s;
|
|
216
|
+
}
|
|
185
217
|
function searchThaiAddress(index, query, options) {
|
|
186
|
-
const limit = options?.limit ?? 10;
|
|
218
|
+
const limit = normalizeLimit(options?.limit ?? 10);
|
|
187
219
|
const threshold = options?.threshold ?? 0.4;
|
|
188
|
-
if (!index || !query) return [];
|
|
220
|
+
if (!index || !index.map || !index.records || typeof query !== "string") return [];
|
|
189
221
|
if (query.length > 1e3) return [];
|
|
190
222
|
const normalized = normalizeThaiAddressText(query);
|
|
191
223
|
if (normalized.length === 0) return [];
|
|
@@ -197,19 +229,25 @@ function searchThaiAddress(index, query, options) {
|
|
|
197
229
|
});
|
|
198
230
|
}
|
|
199
231
|
if (normalized.length < 3) return [];
|
|
232
|
+
if (limit === 0) return [];
|
|
200
233
|
const searchText = options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized) ? applyRomanizationAliases(normalized) : normalized;
|
|
201
234
|
const queryTrigrams = extractTrigramsNormalized(searchText);
|
|
202
|
-
const
|
|
235
|
+
const { counts, touched } = getScratch(index);
|
|
236
|
+
let touchedLen = 0;
|
|
203
237
|
for (const trigram of queryTrigrams) {
|
|
204
238
|
const candidates = index.map.get(trigram);
|
|
205
239
|
if (!candidates) continue;
|
|
206
240
|
for (const idx of candidates) {
|
|
207
|
-
|
|
241
|
+
if (counts[idx] === 0) touched[touchedLen++] = idx;
|
|
242
|
+
counts[idx]++;
|
|
208
243
|
}
|
|
209
244
|
}
|
|
210
245
|
const scored = [];
|
|
211
|
-
|
|
212
|
-
|
|
246
|
+
const totalTrigrams = queryTrigrams.size;
|
|
247
|
+
for (let i = 0; i < touchedLen; i++) {
|
|
248
|
+
const idx = touched[i];
|
|
249
|
+
const score = counts[idx] / totalTrigrams;
|
|
250
|
+
counts[idx] = 0;
|
|
213
251
|
if (score >= threshold) {
|
|
214
252
|
scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) });
|
|
215
253
|
}
|
|
@@ -218,7 +256,11 @@ function searchThaiAddress(index, query, options) {
|
|
|
218
256
|
if (b.score !== a.score) return b.score - a.score;
|
|
219
257
|
return b.matchRank - a.matchRank;
|
|
220
258
|
});
|
|
221
|
-
|
|
259
|
+
let windowSize = Math.min(limit, scored.length);
|
|
260
|
+
const boundary = scored[windowSize - 1];
|
|
261
|
+
while (windowSize < scored.length && scored[windowSize].score === boundary.score && scored[windowSize].matchRank === boundary.matchRank) {
|
|
262
|
+
windowSize++;
|
|
263
|
+
}
|
|
222
264
|
const window = scored.slice(0, windowSize);
|
|
223
265
|
window.sort((a, b) => {
|
|
224
266
|
if (b.score !== a.score) return b.score - a.score;
|
|
@@ -227,7 +269,7 @@ function searchThaiAddress(index, query, options) {
|
|
|
227
269
|
const rb = index.records[b.idx];
|
|
228
270
|
return TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) || TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) || TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh);
|
|
229
271
|
});
|
|
230
|
-
return window.slice(0, limit).map(({ idx }) => index.records[idx]);
|
|
272
|
+
return window.slice(0, limit).map(({ idx }) => ({ ...index.records[idx] }));
|
|
231
273
|
}
|
|
232
274
|
|
|
233
275
|
// src/core/formatter.ts
|
|
@@ -285,8 +327,7 @@ function useThaiAddressAutocomplete(options) {
|
|
|
285
327
|
const [suggestions, setSuggestions] = react.useState([]);
|
|
286
328
|
const timerRef = react.useRef(null);
|
|
287
329
|
const matchedRecordsMapRef = react.useRef(/* @__PURE__ */ new Map());
|
|
288
|
-
const
|
|
289
|
-
const isFirstIndexEffectRunRef = react.useRef(true);
|
|
330
|
+
const suppressedQueryRef = react.useRef(initialQuery);
|
|
290
331
|
const runSearch = react.useCallback(
|
|
291
332
|
(q, idx) => {
|
|
292
333
|
const records = searchThaiAddress(idx, q, { limit, threshold, zipLimit });
|
|
@@ -307,10 +348,8 @@ function useThaiAddressAutocomplete(options) {
|
|
|
307
348
|
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
308
349
|
return;
|
|
309
350
|
}
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
351
|
+
if (query === suppressedQueryRef.current) return;
|
|
352
|
+
suppressedQueryRef.current = "";
|
|
314
353
|
timerRef.current = setTimeout(() => {
|
|
315
354
|
runSearch(query, indexRef.current);
|
|
316
355
|
}, debounceMs);
|
|
@@ -319,15 +358,8 @@ function useThaiAddressAutocomplete(options) {
|
|
|
319
358
|
};
|
|
320
359
|
}, [query, limit, threshold, debounceMs, zipLimit, locale, runSearch]);
|
|
321
360
|
react.useEffect(() => {
|
|
322
|
-
if (isFirstIndexEffectRunRef.current) {
|
|
323
|
-
isFirstIndexEffectRunRef.current = false;
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
361
|
if (query.length === 0) return;
|
|
327
|
-
if (
|
|
328
|
-
suppressNextSearchRef.current = false;
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
362
|
+
if (query === suppressedQueryRef.current) return;
|
|
331
363
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
332
364
|
runSearch(query, index);
|
|
333
365
|
}, [index]);
|
|
@@ -353,7 +385,7 @@ function useThaiAddressAutocomplete(options) {
|
|
|
353
385
|
}, []);
|
|
354
386
|
const setQuerySilent = react.useCallback((value) => {
|
|
355
387
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
356
|
-
|
|
388
|
+
suppressedQueryRef.current = value;
|
|
357
389
|
setSuggestions([]);
|
|
358
390
|
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
359
391
|
setQuery(value);
|
package/dist/react.d.cts
CHANGED
|
@@ -16,6 +16,15 @@ type TrigramIndex = {
|
|
|
16
16
|
map: Map<string, Set<number>>;
|
|
17
17
|
records: ThaiAddressRecord[];
|
|
18
18
|
zipIndex: Map<string, number[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Zip codes of `zipIndex` in ascending lexical order, with postings
|
|
21
|
+
* parallel in `sortedZipPostings`. Used by `lookupByZipCode` for
|
|
22
|
+
* O(log n + matches) prefix lookup instead of scanning every zip code.
|
|
23
|
+
* Optional: older-shaped or hand-built indexes without these fields fall
|
|
24
|
+
* back to the O(n_zips) scan.
|
|
25
|
+
*/
|
|
26
|
+
sortedZipKeys?: string[];
|
|
27
|
+
sortedZipPostings?: number[][];
|
|
19
28
|
/**
|
|
20
29
|
* Normalized Thai tambon names, parallel to `records` (same index).
|
|
21
30
|
* Used by the search ranker for exact/prefix match detection without
|
package/dist/react.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ type TrigramIndex = {
|
|
|
16
16
|
map: Map<string, Set<number>>;
|
|
17
17
|
records: ThaiAddressRecord[];
|
|
18
18
|
zipIndex: Map<string, number[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Zip codes of `zipIndex` in ascending lexical order, with postings
|
|
21
|
+
* parallel in `sortedZipPostings`. Used by `lookupByZipCode` for
|
|
22
|
+
* O(log n + matches) prefix lookup instead of scanning every zip code.
|
|
23
|
+
* Optional: older-shaped or hand-built indexes without these fields fall
|
|
24
|
+
* back to the O(n_zips) scan.
|
|
25
|
+
*/
|
|
26
|
+
sortedZipKeys?: string[];
|
|
27
|
+
sortedZipPostings?: number[][];
|
|
19
28
|
/**
|
|
20
29
|
* Normalized Thai tambon names, parallel to `records` (same index).
|
|
21
30
|
* Used by the search ranker for exact/prefix match detection without
|
package/dist/react.js
CHANGED
|
@@ -5,8 +5,13 @@ import { useRef, useState, useCallback, useEffect } from 'react';
|
|
|
5
5
|
var TONE_MARKS = /[่-๋็์]/g;
|
|
6
6
|
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
|
|
7
7
|
function normalizeThaiAddressText(input) {
|
|
8
|
-
if (
|
|
9
|
-
|
|
8
|
+
if (typeof input !== "string") {
|
|
9
|
+
throw new TypeError(
|
|
10
|
+
`[thaizip] normalizeThaiAddressText expected string, got ${input === null ? "null" : typeof input}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
if (input.length === 0) return "";
|
|
14
|
+
let text = input.normalize("NFC").trim();
|
|
10
15
|
text = text.replace(ADDRESS_PREFIXES, "").trim();
|
|
11
16
|
text = text.replace(TONE_MARKS, "");
|
|
12
17
|
text = text.toLowerCase();
|
|
@@ -145,27 +150,44 @@ function compareZip(a, b) {
|
|
|
145
150
|
if (a > b) return 1;
|
|
146
151
|
return 0;
|
|
147
152
|
}
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return compareZip(a.zipCode, b.zipCode);
|
|
153
|
-
});
|
|
154
|
-
return matches;
|
|
153
|
+
function normalizeLimit(value) {
|
|
154
|
+
if (Number.isNaN(value) || value === -Infinity) return 0;
|
|
155
|
+
if (value === Infinity) return value;
|
|
156
|
+
return Math.max(0, Math.floor(value));
|
|
155
157
|
}
|
|
156
158
|
function lookupByZipCode(index, zip, options) {
|
|
157
|
-
if (!index || !zip) return [];
|
|
159
|
+
if (!index || !index.zipIndex || !index.records || typeof zip !== "string") return [];
|
|
160
|
+
if (zip.length > 1e3) return [];
|
|
161
|
+
const zipLimit = normalizeLimit(options?.zipLimit ?? Infinity);
|
|
162
|
+
if (zipLimit === 0) return [];
|
|
158
163
|
const normalized = zip.trim();
|
|
159
164
|
if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return [];
|
|
160
|
-
const zipLimit = options?.zipLimit ?? Infinity;
|
|
161
165
|
const matches = [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
const keys = index.sortedZipKeys;
|
|
167
|
+
const postings = index.sortedZipPostings;
|
|
168
|
+
if (keys !== void 0 && postings !== void 0) {
|
|
169
|
+
let lo = 0;
|
|
170
|
+
let hi = keys.length;
|
|
171
|
+
while (lo < hi) {
|
|
172
|
+
const mid = lo + hi >> 1;
|
|
173
|
+
if (keys[mid] < normalized) lo = mid + 1;
|
|
174
|
+
else hi = mid;
|
|
175
|
+
}
|
|
176
|
+
for (let i = lo; i < keys.length && keys[i].startsWith(normalized) && matches.length < zipLimit; i++) {
|
|
177
|
+
const indices = postings[i];
|
|
178
|
+
for (let j = 0; j < indices.length && matches.length < zipLimit; j++) matches.push(index.records[indices[j]]);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
const matchingKeys = [...index.zipIndex.keys()].filter((z) => z.startsWith(normalized)).sort(compareZip);
|
|
182
|
+
for (const z of matchingKeys) {
|
|
183
|
+
if (matches.length >= zipLimit) break;
|
|
184
|
+
for (const idx of index.zipIndex.get(z)) {
|
|
185
|
+
if (matches.length >= zipLimit) break;
|
|
186
|
+
matches.push(index.records[idx]);
|
|
187
|
+
}
|
|
165
188
|
}
|
|
166
189
|
}
|
|
167
|
-
|
|
168
|
-
return matches.slice(0, zipLimit);
|
|
190
|
+
return matches.slice(0, zipLimit).map((record) => ({ ...record }));
|
|
169
191
|
}
|
|
170
192
|
function rankAgainstName(name, query) {
|
|
171
193
|
if (name === void 0 || name.length === 0) return 0;
|
|
@@ -180,10 +202,20 @@ function computeMatchRank(index, idx, query) {
|
|
|
180
202
|
const en = rankAgainstName(index.normTambonEn?.[idx], query);
|
|
181
203
|
return en > th ? en : th;
|
|
182
204
|
}
|
|
205
|
+
var scratchByIndex = /* @__PURE__ */ new WeakMap();
|
|
206
|
+
function getScratch(index) {
|
|
207
|
+
const n = index.records.length;
|
|
208
|
+
let s = scratchByIndex.get(index);
|
|
209
|
+
if (s === void 0 || s.counts.length < n) {
|
|
210
|
+
s = { counts: new Uint32Array(n), touched: new Int32Array(n) };
|
|
211
|
+
scratchByIndex.set(index, s);
|
|
212
|
+
}
|
|
213
|
+
return s;
|
|
214
|
+
}
|
|
183
215
|
function searchThaiAddress(index, query, options) {
|
|
184
|
-
const limit = options?.limit ?? 10;
|
|
216
|
+
const limit = normalizeLimit(options?.limit ?? 10);
|
|
185
217
|
const threshold = options?.threshold ?? 0.4;
|
|
186
|
-
if (!index || !query) return [];
|
|
218
|
+
if (!index || !index.map || !index.records || typeof query !== "string") return [];
|
|
187
219
|
if (query.length > 1e3) return [];
|
|
188
220
|
const normalized = normalizeThaiAddressText(query);
|
|
189
221
|
if (normalized.length === 0) return [];
|
|
@@ -195,19 +227,25 @@ function searchThaiAddress(index, query, options) {
|
|
|
195
227
|
});
|
|
196
228
|
}
|
|
197
229
|
if (normalized.length < 3) return [];
|
|
230
|
+
if (limit === 0) return [];
|
|
198
231
|
const searchText = options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized) ? applyRomanizationAliases(normalized) : normalized;
|
|
199
232
|
const queryTrigrams = extractTrigramsNormalized(searchText);
|
|
200
|
-
const
|
|
233
|
+
const { counts, touched } = getScratch(index);
|
|
234
|
+
let touchedLen = 0;
|
|
201
235
|
for (const trigram of queryTrigrams) {
|
|
202
236
|
const candidates = index.map.get(trigram);
|
|
203
237
|
if (!candidates) continue;
|
|
204
238
|
for (const idx of candidates) {
|
|
205
|
-
|
|
239
|
+
if (counts[idx] === 0) touched[touchedLen++] = idx;
|
|
240
|
+
counts[idx]++;
|
|
206
241
|
}
|
|
207
242
|
}
|
|
208
243
|
const scored = [];
|
|
209
|
-
|
|
210
|
-
|
|
244
|
+
const totalTrigrams = queryTrigrams.size;
|
|
245
|
+
for (let i = 0; i < touchedLen; i++) {
|
|
246
|
+
const idx = touched[i];
|
|
247
|
+
const score = counts[idx] / totalTrigrams;
|
|
248
|
+
counts[idx] = 0;
|
|
211
249
|
if (score >= threshold) {
|
|
212
250
|
scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) });
|
|
213
251
|
}
|
|
@@ -216,7 +254,11 @@ function searchThaiAddress(index, query, options) {
|
|
|
216
254
|
if (b.score !== a.score) return b.score - a.score;
|
|
217
255
|
return b.matchRank - a.matchRank;
|
|
218
256
|
});
|
|
219
|
-
|
|
257
|
+
let windowSize = Math.min(limit, scored.length);
|
|
258
|
+
const boundary = scored[windowSize - 1];
|
|
259
|
+
while (windowSize < scored.length && scored[windowSize].score === boundary.score && scored[windowSize].matchRank === boundary.matchRank) {
|
|
260
|
+
windowSize++;
|
|
261
|
+
}
|
|
220
262
|
const window = scored.slice(0, windowSize);
|
|
221
263
|
window.sort((a, b) => {
|
|
222
264
|
if (b.score !== a.score) return b.score - a.score;
|
|
@@ -225,7 +267,7 @@ function searchThaiAddress(index, query, options) {
|
|
|
225
267
|
const rb = index.records[b.idx];
|
|
226
268
|
return TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) || TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) || TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh);
|
|
227
269
|
});
|
|
228
|
-
return window.slice(0, limit).map(({ idx }) => index.records[idx]);
|
|
270
|
+
return window.slice(0, limit).map(({ idx }) => ({ ...index.records[idx] }));
|
|
229
271
|
}
|
|
230
272
|
|
|
231
273
|
// src/core/formatter.ts
|
|
@@ -283,8 +325,7 @@ function useThaiAddressAutocomplete(options) {
|
|
|
283
325
|
const [suggestions, setSuggestions] = useState([]);
|
|
284
326
|
const timerRef = useRef(null);
|
|
285
327
|
const matchedRecordsMapRef = useRef(/* @__PURE__ */ new Map());
|
|
286
|
-
const
|
|
287
|
-
const isFirstIndexEffectRunRef = useRef(true);
|
|
328
|
+
const suppressedQueryRef = useRef(initialQuery);
|
|
288
329
|
const runSearch = useCallback(
|
|
289
330
|
(q, idx) => {
|
|
290
331
|
const records = searchThaiAddress(idx, q, { limit, threshold, zipLimit });
|
|
@@ -305,10 +346,8 @@ function useThaiAddressAutocomplete(options) {
|
|
|
305
346
|
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
306
347
|
return;
|
|
307
348
|
}
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
349
|
+
if (query === suppressedQueryRef.current) return;
|
|
350
|
+
suppressedQueryRef.current = "";
|
|
312
351
|
timerRef.current = setTimeout(() => {
|
|
313
352
|
runSearch(query, indexRef.current);
|
|
314
353
|
}, debounceMs);
|
|
@@ -317,15 +356,8 @@ function useThaiAddressAutocomplete(options) {
|
|
|
317
356
|
};
|
|
318
357
|
}, [query, limit, threshold, debounceMs, zipLimit, locale, runSearch]);
|
|
319
358
|
useEffect(() => {
|
|
320
|
-
if (isFirstIndexEffectRunRef.current) {
|
|
321
|
-
isFirstIndexEffectRunRef.current = false;
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
359
|
if (query.length === 0) return;
|
|
325
|
-
if (
|
|
326
|
-
suppressNextSearchRef.current = false;
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
360
|
+
if (query === suppressedQueryRef.current) return;
|
|
329
361
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
330
362
|
runSearch(query, index);
|
|
331
363
|
}, [index]);
|
|
@@ -351,7 +383,7 @@ function useThaiAddressAutocomplete(options) {
|
|
|
351
383
|
}, []);
|
|
352
384
|
const setQuerySilent = useCallback((value) => {
|
|
353
385
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
354
|
-
|
|
386
|
+
suppressedQueryRef.current = value;
|
|
355
387
|
setSuggestions([]);
|
|
356
388
|
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
357
389
|
setQuery(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thaizip",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Fast, fuzzy Thai address autocomplete library (headless + React)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"thai",
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"module": "./dist/index.js",
|
|
41
41
|
"types": "./dist/index.d.ts",
|
|
42
42
|
"files": [
|
|
43
|
-
"dist"
|
|
43
|
+
"dist/*.js",
|
|
44
|
+
"dist/*.cjs",
|
|
45
|
+
"dist/*.d.ts",
|
|
46
|
+
"dist/*.d.cts"
|
|
44
47
|
],
|
|
45
48
|
"sideEffects": false,
|
|
46
49
|
"scripts": {
|
|
@@ -55,14 +58,17 @@
|
|
|
55
58
|
"@testing-library/react": "^16.3.2",
|
|
56
59
|
"@types/node": "^25.5.2",
|
|
57
60
|
"@types/react": "^19.2.14",
|
|
58
|
-
"@vitest/coverage-v8": "^4.1.
|
|
61
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
59
62
|
"jsdom": "^29.0.1",
|
|
60
63
|
"react": "^19.2.4",
|
|
61
64
|
"react-dom": "^19.2.4",
|
|
62
65
|
"tsup": "^8.5.1",
|
|
63
|
-
"tsx": "^4.
|
|
66
|
+
"tsx": "^4.23.13",
|
|
64
67
|
"typescript": "^6.0.2",
|
|
65
|
-
"vitest": "^4.1.
|
|
68
|
+
"vitest": "^4.1.11"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"esbuild": "^0.28.1"
|
|
66
72
|
},
|
|
67
73
|
"peerDependencies": {
|
|
68
74
|
"react": ">=18.0.0"
|
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/romanize.ts","../src/core/search.ts","../src/core/enumerate.ts","../src/core/formatter.ts","../src/core/resolver.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams, extractTrigramsNormalized } from './trigrams'\nimport { normalizeThaiAddressText } from './normalizer'\nimport type { BuildIndexOptions, RawData, RawTambon, ThaiAddressRecord, TrigramIndex } from '../types'\n\nfunction typeErr(table: string, id: unknown, field: string, expected: string, value: unknown): TypeError {\n return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`)\n}\n\n/**\n * Validate that a `RawData` payload has the runtime shapes `buildThaiAddressIndex`\n * expects. Intended for consumers building an index from their own data (CSV, CMS,\n * private datasets) who want to fail fast with a descriptive error instead of an\n * opaque crash deep inside the normalizer, or silent `\"undefined\"` labels.\n *\n * Throws a `TypeError` (message prefixed `[thaizip]`) naming the table, the\n * offending record's `id`, the field, and the actual `typeof` on the first bad\n * field found. Does not filter soft-deleted rows first — validates every row as\n * provided.\n */\nexport function validateRawData(data: RawData): void {\n const { provinces, amphures, tambons } = data\n\n for (let i = 0; i < provinces.length; i++) {\n const p = provinces[i]\n if (typeof p.id !== 'number') throw typeErr('province', p.id, 'id', 'number', p.id)\n if (typeof p.name_th !== 'string') throw typeErr('province', p.id, 'name_th', 'string', p.name_th)\n if (typeof p.name_en !== 'string') throw typeErr('province', p.id, 'name_en', 'string', p.name_en)\n }\n\n for (let i = 0; i < amphures.length; i++) {\n const a = amphures[i]\n if (typeof a.id !== 'number') throw typeErr('amphure', a.id, 'id', 'number', a.id)\n if (typeof a.name_th !== 'string') throw typeErr('amphure', a.id, 'name_th', 'string', a.name_th)\n if (typeof a.name_en !== 'string') throw typeErr('amphure', a.id, 'name_en', 'string', a.name_en)\n if (typeof a.province_id !== 'number') throw typeErr('amphure', a.id, 'province_id', 'number', a.province_id)\n }\n\n for (let i = 0; i < tambons.length; i++) {\n const t = tambons[i]\n if (typeof t.id !== 'number') throw typeErr('tambon', t.id, 'id', 'number', t.id)\n if (typeof t.name_th !== 'string') throw typeErr('tambon', t.id, 'name_th', 'string', t.name_th)\n if (typeof t.name_en !== 'string') throw typeErr('tambon', t.id, 'name_en', 'string', t.name_en)\n const zipType = typeof t.zip_code\n if (zipType !== 'string' && zipType !== 'number') {\n throw typeErr('tambon', t.id, 'zip_code', 'string or number', t.zip_code)\n }\n if (typeof t.amphure_id !== 'number') throw typeErr('tambon', t.id, 'amphure_id', 'number', t.amphure_id)\n }\n}\n\nfunction addTrigrams(map: Map<string, Set<number>>, trigrams: Set<string>, idx: number): void {\n for (const trigram of trigrams) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n}\n\nfunction combinedTrigrams(nameTh: string, nameEn: string): Set<string> {\n const s = new Set<string>()\n for (const t of extractTrigrams(nameTh)) s.add(t)\n for (const t of extractTrigrams(nameEn)) s.add(t)\n return s\n}\n\nexport function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex {\n if (options?.validate !== false) {\n validateRawData(data)\n }\n\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n // Pre-compute trigrams for shared province and amphure fields to avoid\n // redundant normalization when multiple tambons share the same parent.\n const provTrigrams = new Map<number, Set<string>>()\n for (const [id, prov] of provMap) {\n provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en))\n }\n const ampTrigrams = new Map<number, Set<string>>()\n for (const [id, amp] of ampMap) {\n ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en))\n }\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n const normTambon: string[] = []\n const normTambonEn: string[] = []\n const byProvince = new Map<number, number[]>()\n const byAmphure = new Map<number, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n // Parent groupings, for the enumeration API (cascade selects)\n const provList = byProvince.get(province.id)\n if (provList) provList.push(idx)\n else byProvince.set(province.id, [idx])\n const ampList = byAmphure.get(amphure.id)\n if (ampList) ampList.push(idx)\n else byAmphure.set(amphure.id, [idx])\n\n // Tambon-specific fields (unique per record). The normalized Thai name is\n // kept for the search ranker, and reused here so it is only computed once.\n const normTh = normalizeThaiAddressText(record.tambonNameTh)\n normTambon.push(normTh)\n addTrigrams(map, extractTrigramsNormalized(normTh), idx)\n const normEn = normalizeThaiAddressText(record.tambonNameEn)\n normTambonEn.push(normEn)\n addTrigrams(map, extractTrigramsNormalized(normEn), idx)\n addTrigrams(map, extractTrigrams(record.zipCode), idx)\n // Reuse pre-computed province and amphure trigrams\n addTrigrams(map, provTrigrams.get(province.id)!, idx)\n addTrigrams(map, ampTrigrams.get(amphure.id)!, idx)\n }\n\n return { map, records, zipIndex, normTambon, normTambonEn, byProvince, byAmphure }\n}\n","/**\n * Romanization alias expansion for Latin-script queries.\n *\n * The bundled dataset indexes only the official RTGS transliteration of each\n * name (`name_en`), so common but non-RTGS spellings a user actually types\n * (\"lardprao\", \"krungthep\", \"sukumvit\") miss the index entirely. This module\n * rewrites such a query into its RTGS form before trigram extraction.\n *\n * Contract:\n * - Input is an ALREADY-NORMALIZED query (lowercased, prefixes and tone marks\n * stripped) as produced by `normalizeThaiAddressText`.\n * - Output is a normalized query too — safe to hand straight to\n * `extractTrigramsNormalized`.\n * - Thai-script input must be returned unchanged.\n * - Must be a pure function; it runs on every keystroke.\n *\n * Design (two layers, cheapest/most-reliable first):\n *\n * 1. A curated dictionary of non-RTGS spelling -> exact RTGS string, as it\n * literally appears in a `name_en` field of the bundled dataset. Targets\n * were read off the real generated index (see the romanize benchmark\n * harness used to build this list), not guessed — a couple of entries\n * intentionally map onto data typos already baked into the dataset\n * (`loburi`, `buogkan`) because that is what is actually indexed.\n *\n * 2. A tiny, conservative cleanup pass used only when the dictionary misses:\n * collapse runs of whitespace, and strip a few English administrative\n * words (`district`, `province`, ...) that never occur inside a real\n * `name_en` value. Both were verified against the full dataset before\n * being added — see the \"rejected rules\" note below for transformations\n * that were tried and thrown out because they collide with real names.\n *\n * Rejected systematic rules (measured against the real dataset, not\n * hypothetical): a blanket `klong` -> `khlong` rewrite would break the real\n * tambons \"Mae Klong\" and \"Ho Klong\"; `muang` -> `mueang` would break ~122\n * real tambons that legitimately contain \"Muang\" as a name component (e.g.\n * \"Bang Muang\", \"Khok Muang\"); `huay` -> `huai` would break the real tambon\n * \"Huay Lueg\"; a blanket hyphen -> space rewrite would break the ~70 real\n * names that use a hyphen on purpose (e.g. \"Bang Pa-in\", the Ayutthaya\n * palace town). None of the classic AUA-vs-RTGS letter swaps (`oo` -> `u`,\n * trailing `-rd` -> `-t`, `dt` -> `t`, `bp` -> `p`, doubled consonants) had\n * any real occurrence in the traffic-prioritized query set used to validate\n * this module, so none were added — see the harness report for the\n * before/after numbers this file was tuned against.\n */\n\n// --- Layer 1: curated alias dictionary -------------------------------------\n//\n// Keys are already-normalized (lowercase) non-RTGS spellings. Values are the\n// exact RTGS substring as it appears in the dataset's `name_en` fields\n// (verified against the real generated index, not guessed).\nconst ALIASES: ReadonlyMap<string, string> = new Map([\n // --- Bangkok khet (districts) — concatenated (\"no space\") forms ---------\n ['phranakhon', 'phra nakhon'],\n ['nongchok', 'nong chok'],\n ['bangrak', 'bang rak'],\n ['bangkhen', 'bang khen'],\n ['bangkapi', 'bang kapi'],\n ['pathumwan', 'pathum wan'],\n ['patumwan', 'pathum wan'],\n ['pomprap', 'pom prap sattru phai'],\n ['phrakhanong', 'phra khanong'],\n ['minburi', 'min buri'],\n ['latkrabang', 'lat krabang'],\n ['ladkrabang', 'lat krabang'],\n ['yannawa', 'yan nawa'],\n ['phayathai', 'phaya thai'],\n ['thonburi', 'thon buri'],\n ['bangkokyai', 'bangkok yai'],\n ['huaikhwang', 'huai khwang'],\n ['huaykhwang', 'huai khwang'],\n ['huaykwang', 'huai khwang'],\n ['khlongsan', 'khlong san'],\n ['klongsan', 'khlong san'],\n ['talingchan', 'taling chan'],\n ['bangkoknoi', 'bangkok noi'],\n ['bangkhunthian', 'bang khun thian'],\n ['phasicharoen', 'phasi charoen'],\n ['nongkhaem', 'nong khaem'],\n ['ratburana', 'rat burana'],\n ['bangphlat', 'bang phlat'],\n ['dindaeng', 'din daeng'],\n ['buengkum', 'bueng kum'],\n ['bangsue', 'bang sue'],\n ['bangkholaem', 'bang kho laem'],\n ['khlongtoei', 'khlong toei'],\n ['klongtoei', 'khlong toei'],\n ['klongtoey', 'khlong toei'],\n ['khlongtoey', 'khlong toei'],\n ['suanluang', 'suan luang'],\n ['chomthong', 'chom thong'],\n ['donmueang', 'don mueang'],\n ['donmuang', 'don mueang'],\n ['latphrao', 'lat phrao'],\n ['ladprao', 'lat phrao'],\n ['lardprao', 'lat phrao'],\n ['lat prao', 'lat phrao'],\n ['bangkhae', 'bang khae'],\n ['laksi', 'lak si'],\n ['saimai', 'sai mai'],\n ['khannayao', 'khan na yao'],\n ['saphansung', 'saphan sung'],\n ['wangthonglang', 'wang thonglang'],\n ['khlongsamwa', 'khlong sam wa'],\n ['klongsamwa', 'khlong sam wa'],\n ['bangna', 'bang na'],\n ['thawiwatthana', 'thawi watthana'],\n ['thungkhru', 'thung khru'],\n ['bangbon', 'bang bon'],\n\n // --- Bangkok area / street nicknames with a common misspelling ----------\n ['sathorn', 'sathon'],\n ['wattana', 'watthana'],\n ['silom', 'si lom'],\n\n // --- Provinces and other high-traffic areas ------------------------------\n ['krungthep', 'bangkok'],\n ['korat', 'nakhon ratchasima'],\n ['nakornratchasima', 'nakhon ratchasima'],\n ['chiangmai', 'chiang mai'],\n ['chiangrai', 'chiang rai'],\n ['pathumthani', 'pathum thani'],\n ['patumthani', 'pathum thani'],\n ['samutprakan', 'samut prakan'],\n ['samutsakhon', 'samut sakhon'],\n ['samutsongkhram', 'samut songkhram'],\n ['nakhonpathom', 'nakhon pathom'],\n ['nakhonsawan', 'nakhon sawan'],\n ['nakhonsithammarat', 'nakhon si thammarat'],\n ['prachuapkhirikhan', 'prachuap khiri khan'],\n ['huahin', 'hua hin'],\n ['hatyai', 'hat yai'],\n ['hadyai', 'hat yai'],\n ['had yai', 'hat yai'],\n ['lopburi', 'loburi'],\n ['lop buri', 'loburi'],\n ['buengkan', 'buogkan'],\n ['bungkan', 'buogkan'],\n ['bung kan', 'buogkan'],\n ['udonthani', 'udon thani'],\n ['khonkaen', 'khon kaen'],\n ['suphanburi', 'suphan buri'],\n ['nongkhai', 'nong khai'],\n ['buriram', 'buri ram'],\n])\n\n// --- Layer 2: safe systematic cleanup (dictionary-miss fallback only) -----\n//\n// English administrative words that NEVER occur inside a real `name_en`\n// value (verified against the full dataset) — safe to strip unconditionally.\n// Deliberately excludes \"khet\" and \"tambon\": both occur as genuine name\n// components in the real data (\"Khet Chatuchak\" as a whole amphure name,\n// but also tambons like \"Sanam Chai Khet\" and \"Sam Tambon\"), so stripping\n// them would corrupt otherwise-correct queries.\nconst NOISE_WORDS_RE = /\\b(?:sub[- ]?district|district|province|changwat|amphoe|amphur)\\b/g\nconst WHITESPACE_RE = /\\s+/g\n\nfunction cleanup(text: string): string {\n const stripped = text.replace(NOISE_WORDS_RE, ' ').replace(WHITESPACE_RE, ' ').trim()\n return stripped.length > 0 ? stripped : text\n}\n\nexport function applyRomanizationAliases(normalized: string): string {\n if (!normalized) return normalized\n\n const direct = ALIASES.get(normalized)\n if (direct !== undefined) return direct\n\n const cleaned = cleanup(normalized)\n if (cleaned === normalized) return normalized\n\n const aliasedCleaned = ALIASES.get(cleaned)\n return aliasedCleaned !== undefined ? aliasedCleaned : cleaned\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport { applyRomanizationAliases } from './romanize'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\nconst HAS_LATIN_RE = /[a-z]/i\n\n// Constructing Intl.Collator is expensive; build it once at module load rather\n// than on every comparator invocation inside a hot sort loop.\nconst TH_COLLATOR = new Intl.Collator('th')\n\n/** Plain, allocation-free string comparison. Zip codes in the bundled dataset\n * are fixed-width zero-padded 5-digit strings, so lexical order already\n * matches numeric order — no need for `localeCompare`'s Intl machinery. */\nfunction compareZip(a: string, b: string): number {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\nfunction sortZipMatches(matches: ThaiAddressRecord[], exactZip: string): ThaiAddressRecord[] {\n matches.sort((a, b) => {\n if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1\n if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1\n return compareZip(a.zipCode, b.zipCode)\n })\n return matches\n}\n\n/**\n * Look up records by zip code (exact or prefix match). Exact matches are\n * returned before prefix matches; ties are ordered by ascending zip code.\n *\n * Unlike `searchThaiAddress`, results are NOT capped by `options.limit` —\n * only by `options.zipLimit` (defaults to `Infinity`), since a zip code can\n * legitimately map to dozens of tambons (e.g. `45000` → 33 records).\n */\nexport function lookupByZipCode(\n index: TrigramIndex,\n zip: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n if (!index || !zip) return []\n const normalized = zip.trim()\n if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return []\n\n const zipLimit = options?.zipLimit ?? Infinity\n\n const matches: ThaiAddressRecord[] = []\n for (const [z, indices] of index.zipIndex) {\n if (z.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n sortZipMatches(matches, normalized)\n return matches.slice(0, zipLimit)\n}\n\nfunction rankAgainstName(name: string | undefined, query: string): number {\n if (name === undefined || name.length === 0) return 0\n if (name === query) return 3\n if (name.startsWith(query)) return 2\n if (name.includes(query)) return 1\n return 0\n}\n\n/** `3` = exact tambon name match, `2` = prefix match, `1` = substring match,\n * `0` = no match on the tambon's own (normalized) name. Used to rank a\n * record whose OWN name matches the query above a record that only matches\n * via its parent amphure/province.\n *\n * Both the Thai and the English name are checked: a Latin-script query can\n * never match the Thai name, so without the English side every romanized\n * query would fall back to alphabetical order and reproduce exactly the\n * ranking bug this function exists to fix. */\nfunction computeMatchRank(index: TrigramIndex, idx: number, query: string): number {\n const th = rankAgainstName(index.normTambon?.[idx], query)\n if (th === 3) return 3\n const en = rankAgainstName(index.normTambonEn?.[idx], query)\n return en > th ? en : th\n}\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n // Bound the raw query before running the (multi-pass) normalizer over it.\n if (query.length > 1000) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n return lookupByZipCode(index, normalized, {\n zipLimit: options?.zipLimit ?? Infinity,\n })\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search. The index never contains\n // trigram keys shorter than 3 chars for names of length >= 3, so a sub-3-char\n // query can never usefully match anything.\n if (normalized.length < 3) return []\n\n // Expand common non-RTGS romanizations (e.g. \"lardprao\" -> \"lat phrao\") for\n // Latin-script queries. Thai-script queries are passed through untouched.\n const searchText =\n options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized)\n ? applyRomanizationAliases(normalized)\n : normalized\n\n const queryTrigrams = extractTrigramsNormalized(searchText)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number; matchRank: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) })\n }\n }\n\n // Cheap numeric pre-sort (score, then matchRank) — no collator, no string\n // comparisons. This must include matchRank, not just score: an exact-name\n // match that ties on score with many parent-only matches could otherwise\n // fall outside the re-sorted window below and be lost.\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n return b.matchRank - a.matchRank\n })\n\n // Only the top window is worth the expense of a locale-aware tie-break;\n // re-sort just that slice with the full comparator (including the\n // module-level cached collator) before truncating to `limit`.\n const windowSize = Math.max(limit * 4, 50)\n const window = scored.slice(0, windowSize)\n window.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n if (b.matchRank !== a.matchRank) return b.matchRank - a.matchRank\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) ||\n TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) ||\n TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh)\n )\n })\n\n return window.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { AmphureSummary, ProvinceSummary, TambonSummary, ThaiAddressRecord, TrigramIndex } from '../types'\n\n// Sorting Thai names inside a comparator must never construct a new Collator\n// or call `localeCompare` with a locale argument per-call — that is the exact\n// PERF-1 hot-path bug fixed elsewhere in this codebase. One cached instance.\nconst thCollator = new Intl.Collator('th')\n\nfunction byNameTh(a: { nameTh: string }, b: { nameTh: string }): number {\n return thCollator.compare(a.nameTh, b.nameTh)\n}\n\n/**\n * All provinces present in the index, deduplicated and sorted by Thai name.\n * Backed by `index.byProvince` (O(unique provinces)); falls back to scanning\n * `index.records` if an older-shaped index lacks that map.\n */\nexport function listProvinces(index: TrigramIndex): ProvinceSummary[] {\n const seen = new Map<number, ProvinceSummary>()\n\n if (index.byProvince) {\n for (const [provinceId, recordIndices] of index.byProvince) {\n if (seen.has(provinceId) || recordIndices.length === 0) continue\n const r = index.records[recordIndices[0]]\n seen.set(provinceId, { id: provinceId, nameTh: r.provinceNameTh, nameEn: r.provinceNameEn })\n }\n } else {\n for (const r of index.records) {\n if (!seen.has(r.provinceId)) {\n seen.set(r.provinceId, { id: r.provinceId, nameTh: r.provinceNameTh, nameEn: r.provinceNameEn })\n }\n }\n }\n\n return Array.from(seen.values()).sort(byNameTh)\n}\n\n/**\n * Amphures (districts) within a given province, deduplicated and sorted by\n * Thai name. Unknown `provinceId` returns `[]`. Backed by `index.byProvince`;\n * falls back to scanning `index.records` if an older-shaped index lacks it.\n */\nexport function listAmphures(index: TrigramIndex, provinceId: number): AmphureSummary[] {\n const seen = new Map<number, AmphureSummary>()\n\n const addFrom = (r: ThaiAddressRecord): void => {\n if (!seen.has(r.amphureId)) {\n seen.set(r.amphureId, {\n id: r.amphureId,\n nameTh: r.amphureNameTh,\n nameEn: r.amphureNameEn,\n provinceId: r.provinceId,\n })\n }\n }\n\n if (index.byProvince) {\n const recordIndices = index.byProvince.get(provinceId)\n if (!recordIndices) return []\n for (const idx of recordIndices) addFrom(index.records[idx])\n } else {\n for (const r of index.records) {\n if (r.provinceId === provinceId) addFrom(r)\n }\n }\n\n return Array.from(seen.values()).sort(byNameTh)\n}\n\n/**\n * Tambons (subdistricts) within a given amphure, sorted by Thai name.\n * Unknown `amphureId` returns `[]`. Backed by `index.byAmphure`; falls back\n * to scanning `index.records` if an older-shaped index lacks it. Each\n * `byAmphure` entry already maps to exactly one tambon record, so no\n * deduplication is needed here.\n */\nexport function listTambons(index: TrigramIndex, amphureId: number): TambonSummary[] {\n const toSummary = (r: ThaiAddressRecord): TambonSummary => ({\n id: r.tambonId,\n nameTh: r.tambonNameTh,\n nameEn: r.tambonNameEn,\n amphureId: r.amphureId,\n zipCode: r.zipCode,\n })\n\n let result: TambonSummary[]\n\n if (index.byAmphure) {\n const recordIndices = index.byAmphure.get(amphureId)\n if (!recordIndices) return []\n result = recordIndices.map(idx => toSummary(index.records[idx]))\n } else {\n result = index.records.filter(r => r.amphureId === amphureId).map(toSummary)\n }\n\n return result.sort(byNameTh)\n}\n","import type {\n FormatSuggestionOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n} from '../types'\n\nexport function formatThaiAddressSuggestion(\n record: ThaiAddressRecord,\n options?: FormatSuggestionOptions,\n): ThaiAddressSuggestion {\n const labelTh = `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`\n const labelEn = `${record.tambonNameEn} > ${record.amphureNameEn} > ${record.provinceNameEn} ${record.zipCode}`\n\n return {\n id: String(record.tambonId),\n label: options?.locale === 'en' ? labelEn : labelTh,\n labelTh,\n labelEn,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n"]}
|