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/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 (!input) return "";
11
- let text = input.trim();
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 sortZipMatches(matches, exactZip) {
151
- matches.sort((a, b) => {
152
- if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1;
153
- if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1;
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
- for (const [z, indices] of index.zipIndex) {
165
- if (z.startsWith(normalized)) {
166
- for (const idx of indices) matches.push(index.records[idx]);
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
- sortZipMatches(matches, normalized);
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 hits = /* @__PURE__ */ new Map();
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
- hits.set(idx, (hits.get(idx) ?? 0) + 1);
241
+ if (counts[idx] === 0) touched[touchedLen++] = idx;
242
+ counts[idx]++;
208
243
  }
209
244
  }
210
245
  const scored = [];
211
- for (const [idx, count] of hits) {
212
- const score = count / queryTrigrams.size;
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
- const windowSize = Math.max(limit * 4, 50);
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 suppressNextSearchRef = react.useRef(initialQuery.length > 0);
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 (suppressNextSearchRef.current) {
311
- suppressNextSearchRef.current = false;
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 (suppressNextSearchRef.current) {
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
- if (value.length > 0) suppressNextSearchRef.current = true;
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 (!input) return "";
9
- let text = input.trim();
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 sortZipMatches(matches, exactZip) {
149
- matches.sort((a, b) => {
150
- if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1;
151
- if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1;
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
- for (const [z, indices] of index.zipIndex) {
163
- if (z.startsWith(normalized)) {
164
- for (const idx of indices) matches.push(index.records[idx]);
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
- sortZipMatches(matches, normalized);
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 hits = /* @__PURE__ */ new Map();
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
- hits.set(idx, (hits.get(idx) ?? 0) + 1);
239
+ if (counts[idx] === 0) touched[touchedLen++] = idx;
240
+ counts[idx]++;
206
241
  }
207
242
  }
208
243
  const scored = [];
209
- for (const [idx, count] of hits) {
210
- const score = count / queryTrigrams.size;
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
- const windowSize = Math.max(limit * 4, 50);
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 suppressNextSearchRef = useRef(initialQuery.length > 0);
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 (suppressNextSearchRef.current) {
309
- suppressNextSearchRef.current = false;
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 (suppressNextSearchRef.current) {
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
- if (value.length > 0) suppressNextSearchRef.current = true;
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.2",
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.2",
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.21.0",
66
+ "tsx": "^4.23.13",
64
67
  "typescript": "^6.0.2",
65
- "vitest": "^4.1.2"
68
+ "vitest": "^4.1.11"
69
+ },
70
+ "overrides": {
71
+ "esbuild": "^0.28.1"
66
72
  },
67
73
  "peerDependencies": {
68
74
  "react": ">=18.0.0"
@@ -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"]}