thaizip 0.6.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/data.js CHANGED
@@ -54,6 +54,36 @@ function extractTrigramsNormalized(text) {
54
54
  }
55
55
 
56
56
  // src/core/indexer.ts
57
+ function typeErr(table, id, field, expected, value) {
58
+ return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
59
+ }
60
+ function validateRawData(data) {
61
+ const { provinces, amphures, tambons } = data;
62
+ for (let i = 0; i < provinces.length; i++) {
63
+ const p2 = provinces[i];
64
+ if (typeof p2.id !== "number") throw typeErr("province", p2.id, "id", "number", p2.id);
65
+ if (typeof p2.name_th !== "string") throw typeErr("province", p2.id, "name_th", "string", p2.name_th);
66
+ if (typeof p2.name_en !== "string") throw typeErr("province", p2.id, "name_en", "string", p2.name_en);
67
+ }
68
+ for (let i = 0; i < amphures.length; i++) {
69
+ const a2 = amphures[i];
70
+ if (typeof a2.id !== "number") throw typeErr("amphure", a2.id, "id", "number", a2.id);
71
+ if (typeof a2.name_th !== "string") throw typeErr("amphure", a2.id, "name_th", "string", a2.name_th);
72
+ if (typeof a2.name_en !== "string") throw typeErr("amphure", a2.id, "name_en", "string", a2.name_en);
73
+ if (typeof a2.province_id !== "number") throw typeErr("amphure", a2.id, "province_id", "number", a2.province_id);
74
+ }
75
+ for (let i = 0; i < tambons.length; i++) {
76
+ const t2 = tambons[i];
77
+ if (typeof t2.id !== "number") throw typeErr("tambon", t2.id, "id", "number", t2.id);
78
+ if (typeof t2.name_th !== "string") throw typeErr("tambon", t2.id, "name_th", "string", t2.name_th);
79
+ if (typeof t2.name_en !== "string") throw typeErr("tambon", t2.id, "name_en", "string", t2.name_en);
80
+ const zipType = typeof t2.zip_code;
81
+ if (zipType !== "string" && zipType !== "number") {
82
+ throw typeErr("tambon", t2.id, "zip_code", "string or number", t2.zip_code);
83
+ }
84
+ if (typeof t2.amphure_id !== "number") throw typeErr("tambon", t2.id, "amphure_id", "number", t2.amphure_id);
85
+ }
86
+ }
57
87
  function addTrigrams(map, trigrams, idx) {
58
88
  for (const trigram of trigrams) {
59
89
  let set = map.get(trigram);
@@ -71,6 +101,9 @@ function combinedTrigrams(nameTh, nameEn) {
71
101
  return s;
72
102
  }
73
103
  function buildThaiAddressIndex(data, options) {
104
+ if (options?.validate !== false) {
105
+ validateRawData(data);
106
+ }
74
107
  const { provinces, amphures, tambons } = data;
75
108
  const provMap = new Map(
76
109
  provinces.filter((p2) => !p2.deleted_at).map((p2) => [p2.id, p2])
@@ -89,14 +122,20 @@ function buildThaiAddressIndex(data, options) {
89
122
  const records = [];
90
123
  const map = /* @__PURE__ */ new Map();
91
124
  const zipIndex = /* @__PURE__ */ new Map();
125
+ const normTambon = [];
126
+ const normTambonEn = [];
127
+ const byProvince = /* @__PURE__ */ new Map();
128
+ const byAmphure = /* @__PURE__ */ new Map();
92
129
  for (const tambon of tambons) {
93
130
  if (tambon.deleted_at) continue;
94
131
  const amphure = ampMap.get(tambon.amphure_id);
95
132
  if (!amphure) {
133
+ if (options?.onSkip) options.onSkip(tambon);
96
134
  continue;
97
135
  }
98
136
  const province = provMap.get(amphure.province_id);
99
137
  if (!province) {
138
+ if (options?.onSkip) options.onSkip(tambon);
100
139
  continue;
101
140
  }
102
141
  const record = {
@@ -116,13 +155,23 @@ function buildThaiAddressIndex(data, options) {
116
155
  const existing = zipIndex.get(record.zipCode);
117
156
  if (existing) existing.push(idx);
118
157
  else zipIndex.set(record.zipCode, [idx]);
119
- addTrigrams(map, extractTrigrams(record.tambonNameTh), idx);
120
- addTrigrams(map, extractTrigrams(record.tambonNameEn), idx);
158
+ const provList = byProvince.get(province.id);
159
+ if (provList) provList.push(idx);
160
+ else byProvince.set(province.id, [idx]);
161
+ const ampList = byAmphure.get(amphure.id);
162
+ if (ampList) ampList.push(idx);
163
+ else byAmphure.set(amphure.id, [idx]);
164
+ const normTh = normalizeThaiAddressText(record.tambonNameTh);
165
+ normTambon.push(normTh);
166
+ addTrigrams(map, extractTrigramsNormalized(normTh), idx);
167
+ const normEn = normalizeThaiAddressText(record.tambonNameEn);
168
+ normTambonEn.push(normEn);
169
+ addTrigrams(map, extractTrigramsNormalized(normEn), idx);
121
170
  addTrigrams(map, extractTrigrams(record.zipCode), idx);
122
171
  addTrigrams(map, provTrigrams.get(province.id), idx);
123
172
  addTrigrams(map, ampTrigrams.get(amphure.id), idx);
124
173
  }
125
- return { map, records, zipIndex };
174
+ return { map, records, zipIndex, normTambon, normTambonEn, byProvince, byAmphure };
126
175
  }
127
176
 
128
177
  // src/data/loader.ts
@@ -143,7 +192,7 @@ async function loadDefaultIndex() {
143
192
  provinces: p2.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
144
193
  amphures: a2.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
145
194
  tambons: t2.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
146
- });
195
+ }, { validate: false });
147
196
  if (loadGeneration === gen) {
148
197
  cached = index;
149
198
  inflightPromise = null;
package/dist/index.cjs CHANGED
@@ -30,6 +30,36 @@ function extractTrigramsNormalized(text) {
30
30
  }
31
31
 
32
32
  // src/core/indexer.ts
33
+ function typeErr(table, id, field, expected, value) {
34
+ return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
35
+ }
36
+ function validateRawData(data) {
37
+ const { provinces, amphures, tambons } = data;
38
+ for (let i = 0; i < provinces.length; i++) {
39
+ const p = provinces[i];
40
+ if (typeof p.id !== "number") throw typeErr("province", p.id, "id", "number", p.id);
41
+ if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
42
+ if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
43
+ }
44
+ for (let i = 0; i < amphures.length; i++) {
45
+ const a = amphures[i];
46
+ if (typeof a.id !== "number") throw typeErr("amphure", a.id, "id", "number", a.id);
47
+ if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
48
+ if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
49
+ if (typeof a.province_id !== "number") throw typeErr("amphure", a.id, "province_id", "number", a.province_id);
50
+ }
51
+ for (let i = 0; i < tambons.length; i++) {
52
+ const t = tambons[i];
53
+ if (typeof t.id !== "number") throw typeErr("tambon", t.id, "id", "number", t.id);
54
+ if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
55
+ if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
56
+ const zipType = typeof t.zip_code;
57
+ if (zipType !== "string" && zipType !== "number") {
58
+ throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
59
+ }
60
+ if (typeof t.amphure_id !== "number") throw typeErr("tambon", t.id, "amphure_id", "number", t.amphure_id);
61
+ }
62
+ }
33
63
  function addTrigrams(map, trigrams, idx) {
34
64
  for (const trigram of trigrams) {
35
65
  let set = map.get(trigram);
@@ -47,6 +77,9 @@ function combinedTrigrams(nameTh, nameEn) {
47
77
  return s;
48
78
  }
49
79
  function buildThaiAddressIndex(data, options) {
80
+ if (options?.validate !== false) {
81
+ validateRawData(data);
82
+ }
50
83
  const { provinces, amphures, tambons } = data;
51
84
  const provMap = new Map(
52
85
  provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
@@ -65,6 +98,10 @@ function buildThaiAddressIndex(data, options) {
65
98
  const records = [];
66
99
  const map = /* @__PURE__ */ new Map();
67
100
  const zipIndex = /* @__PURE__ */ new Map();
101
+ const normTambon = [];
102
+ const normTambonEn = [];
103
+ const byProvince = /* @__PURE__ */ new Map();
104
+ const byAmphure = /* @__PURE__ */ new Map();
68
105
  for (const tambon of tambons) {
69
106
  if (tambon.deleted_at) continue;
70
107
  const amphure = ampMap.get(tambon.amphure_id);
@@ -94,41 +131,195 @@ function buildThaiAddressIndex(data, options) {
94
131
  const existing = zipIndex.get(record.zipCode);
95
132
  if (existing) existing.push(idx);
96
133
  else zipIndex.set(record.zipCode, [idx]);
97
- addTrigrams(map, extractTrigrams(record.tambonNameTh), idx);
98
- addTrigrams(map, extractTrigrams(record.tambonNameEn), idx);
134
+ const provList = byProvince.get(province.id);
135
+ if (provList) provList.push(idx);
136
+ else byProvince.set(province.id, [idx]);
137
+ const ampList = byAmphure.get(amphure.id);
138
+ if (ampList) ampList.push(idx);
139
+ else byAmphure.set(amphure.id, [idx]);
140
+ const normTh = normalizeThaiAddressText(record.tambonNameTh);
141
+ normTambon.push(normTh);
142
+ addTrigrams(map, extractTrigramsNormalized(normTh), idx);
143
+ const normEn = normalizeThaiAddressText(record.tambonNameEn);
144
+ normTambonEn.push(normEn);
145
+ addTrigrams(map, extractTrigramsNormalized(normEn), idx);
99
146
  addTrigrams(map, extractTrigrams(record.zipCode), idx);
100
147
  addTrigrams(map, provTrigrams.get(province.id), idx);
101
148
  addTrigrams(map, ampTrigrams.get(amphure.id), idx);
102
149
  }
103
- return { map, records, zipIndex };
150
+ return { map, records, zipIndex, normTambon, normTambonEn, byProvince, byAmphure };
151
+ }
152
+
153
+ // src/core/romanize.ts
154
+ var ALIASES = /* @__PURE__ */ new Map([
155
+ // --- Bangkok khet (districts) — concatenated ("no space") forms ---------
156
+ ["phranakhon", "phra nakhon"],
157
+ ["nongchok", "nong chok"],
158
+ ["bangrak", "bang rak"],
159
+ ["bangkhen", "bang khen"],
160
+ ["bangkapi", "bang kapi"],
161
+ ["pathumwan", "pathum wan"],
162
+ ["patumwan", "pathum wan"],
163
+ ["pomprap", "pom prap sattru phai"],
164
+ ["phrakhanong", "phra khanong"],
165
+ ["minburi", "min buri"],
166
+ ["latkrabang", "lat krabang"],
167
+ ["ladkrabang", "lat krabang"],
168
+ ["yannawa", "yan nawa"],
169
+ ["phayathai", "phaya thai"],
170
+ ["thonburi", "thon buri"],
171
+ ["bangkokyai", "bangkok yai"],
172
+ ["huaikhwang", "huai khwang"],
173
+ ["huaykhwang", "huai khwang"],
174
+ ["huaykwang", "huai khwang"],
175
+ ["khlongsan", "khlong san"],
176
+ ["klongsan", "khlong san"],
177
+ ["talingchan", "taling chan"],
178
+ ["bangkoknoi", "bangkok noi"],
179
+ ["bangkhunthian", "bang khun thian"],
180
+ ["phasicharoen", "phasi charoen"],
181
+ ["nongkhaem", "nong khaem"],
182
+ ["ratburana", "rat burana"],
183
+ ["bangphlat", "bang phlat"],
184
+ ["dindaeng", "din daeng"],
185
+ ["buengkum", "bueng kum"],
186
+ ["bangsue", "bang sue"],
187
+ ["bangkholaem", "bang kho laem"],
188
+ ["khlongtoei", "khlong toei"],
189
+ ["klongtoei", "khlong toei"],
190
+ ["klongtoey", "khlong toei"],
191
+ ["khlongtoey", "khlong toei"],
192
+ ["suanluang", "suan luang"],
193
+ ["chomthong", "chom thong"],
194
+ ["donmueang", "don mueang"],
195
+ ["donmuang", "don mueang"],
196
+ ["latphrao", "lat phrao"],
197
+ ["ladprao", "lat phrao"],
198
+ ["lardprao", "lat phrao"],
199
+ ["lat prao", "lat phrao"],
200
+ ["bangkhae", "bang khae"],
201
+ ["laksi", "lak si"],
202
+ ["saimai", "sai mai"],
203
+ ["khannayao", "khan na yao"],
204
+ ["saphansung", "saphan sung"],
205
+ ["wangthonglang", "wang thonglang"],
206
+ ["khlongsamwa", "khlong sam wa"],
207
+ ["klongsamwa", "khlong sam wa"],
208
+ ["bangna", "bang na"],
209
+ ["thawiwatthana", "thawi watthana"],
210
+ ["thungkhru", "thung khru"],
211
+ ["bangbon", "bang bon"],
212
+ // --- Bangkok area / street nicknames with a common misspelling ----------
213
+ ["sathorn", "sathon"],
214
+ ["wattana", "watthana"],
215
+ ["silom", "si lom"],
216
+ // --- Provinces and other high-traffic areas ------------------------------
217
+ ["krungthep", "bangkok"],
218
+ ["korat", "nakhon ratchasima"],
219
+ ["nakornratchasima", "nakhon ratchasima"],
220
+ ["chiangmai", "chiang mai"],
221
+ ["chiangrai", "chiang rai"],
222
+ ["pathumthani", "pathum thani"],
223
+ ["patumthani", "pathum thani"],
224
+ ["samutprakan", "samut prakan"],
225
+ ["samutsakhon", "samut sakhon"],
226
+ ["samutsongkhram", "samut songkhram"],
227
+ ["nakhonpathom", "nakhon pathom"],
228
+ ["nakhonsawan", "nakhon sawan"],
229
+ ["nakhonsithammarat", "nakhon si thammarat"],
230
+ ["prachuapkhirikhan", "prachuap khiri khan"],
231
+ ["huahin", "hua hin"],
232
+ ["hatyai", "hat yai"],
233
+ ["hadyai", "hat yai"],
234
+ ["had yai", "hat yai"],
235
+ ["lopburi", "loburi"],
236
+ ["lop buri", "loburi"],
237
+ ["buengkan", "buogkan"],
238
+ ["bungkan", "buogkan"],
239
+ ["bung kan", "buogkan"],
240
+ ["udonthani", "udon thani"],
241
+ ["khonkaen", "khon kaen"],
242
+ ["suphanburi", "suphan buri"],
243
+ ["nongkhai", "nong khai"],
244
+ ["buriram", "buri ram"]
245
+ ]);
246
+ var NOISE_WORDS_RE = /\b(?:sub[- ]?district|district|province|changwat|amphoe|amphur)\b/g;
247
+ var WHITESPACE_RE = /\s+/g;
248
+ function cleanup(text) {
249
+ const stripped = text.replace(NOISE_WORDS_RE, " ").replace(WHITESPACE_RE, " ").trim();
250
+ return stripped.length > 0 ? stripped : text;
251
+ }
252
+ function applyRomanizationAliases(normalized) {
253
+ if (!normalized) return normalized;
254
+ const direct = ALIASES.get(normalized);
255
+ if (direct !== void 0) return direct;
256
+ const cleaned = cleanup(normalized);
257
+ if (cleaned === normalized) return normalized;
258
+ const aliasedCleaned = ALIASES.get(cleaned);
259
+ return aliasedCleaned !== void 0 ? aliasedCleaned : cleaned;
104
260
  }
105
261
 
106
262
  // src/core/search.ts
107
263
  var ZIP_CODE_RE = /^\d+$/;
264
+ var HAS_LATIN_RE = /[a-z]/i;
265
+ var TH_COLLATOR = new Intl.Collator("th");
266
+ function compareZip(a, b) {
267
+ if (a < b) return -1;
268
+ if (a > b) return 1;
269
+ return 0;
270
+ }
271
+ function sortZipMatches(matches, exactZip) {
272
+ matches.sort((a, b) => {
273
+ if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1;
274
+ if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1;
275
+ return compareZip(a.zipCode, b.zipCode);
276
+ });
277
+ return matches;
278
+ }
279
+ function lookupByZipCode(index, zip, options) {
280
+ if (!index || !zip) return [];
281
+ const normalized = zip.trim();
282
+ if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return [];
283
+ const zipLimit = options?.zipLimit ?? Infinity;
284
+ const matches = [];
285
+ for (const [z, indices] of index.zipIndex) {
286
+ if (z.startsWith(normalized)) {
287
+ for (const idx of indices) matches.push(index.records[idx]);
288
+ }
289
+ }
290
+ sortZipMatches(matches, normalized);
291
+ return matches.slice(0, zipLimit);
292
+ }
293
+ function rankAgainstName(name, query) {
294
+ if (name === void 0 || name.length === 0) return 0;
295
+ if (name === query) return 3;
296
+ if (name.startsWith(query)) return 2;
297
+ if (name.includes(query)) return 1;
298
+ return 0;
299
+ }
300
+ function computeMatchRank(index, idx, query) {
301
+ const th = rankAgainstName(index.normTambon?.[idx], query);
302
+ if (th === 3) return 3;
303
+ const en = rankAgainstName(index.normTambonEn?.[idx], query);
304
+ return en > th ? en : th;
305
+ }
108
306
  function searchThaiAddress(index, query, options) {
109
307
  const limit = options?.limit ?? 10;
110
308
  const threshold = options?.threshold ?? 0.4;
111
309
  if (!index || !query) return [];
310
+ if (query.length > 1e3) return [];
112
311
  const normalized = normalizeThaiAddressText(query);
113
312
  if (normalized.length === 0) return [];
114
313
  if (normalized.length > 300) return [];
115
314
  if (ZIP_CODE_RE.test(normalized)) {
116
315
  if (normalized.length < 2) return [];
117
- const matches = [];
118
- for (const [zip, indices] of index.zipIndex) {
119
- if (zip.startsWith(normalized)) {
120
- for (const idx of indices) matches.push(index.records[idx]);
121
- }
122
- }
123
- matches.sort((a, b) => {
124
- if (a.zipCode === normalized && b.zipCode !== normalized) return -1;
125
- if (b.zipCode === normalized && a.zipCode !== normalized) return 1;
126
- return a.zipCode.localeCompare(b.zipCode, void 0, { numeric: true });
316
+ return lookupByZipCode(index, normalized, {
317
+ zipLimit: options?.zipLimit ?? Infinity
127
318
  });
128
- return matches.slice(0, limit);
129
319
  }
130
- if (normalized.length < 3 && query.trim().length < 3) return [];
131
- const queryTrigrams = extractTrigramsNormalized(normalized);
320
+ if (normalized.length < 3) return [];
321
+ const searchText = options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized) ? applyRomanizationAliases(normalized) : normalized;
322
+ const queryTrigrams = extractTrigramsNormalized(searchText);
132
323
  const hits = /* @__PURE__ */ new Map();
133
324
  for (const trigram of queryTrigrams) {
134
325
  const candidates = index.map.get(trigram);
@@ -141,23 +332,98 @@ function searchThaiAddress(index, query, options) {
141
332
  for (const [idx, count] of hits) {
142
333
  const score = count / queryTrigrams.size;
143
334
  if (score >= threshold) {
144
- scored.push({ idx, score });
335
+ scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) });
145
336
  }
146
337
  }
147
338
  scored.sort((a, b) => {
148
339
  if (b.score !== a.score) return b.score - a.score;
340
+ return b.matchRank - a.matchRank;
341
+ });
342
+ const windowSize = Math.max(limit * 4, 50);
343
+ const window = scored.slice(0, windowSize);
344
+ window.sort((a, b) => {
345
+ if (b.score !== a.score) return b.score - a.score;
346
+ if (b.matchRank !== a.matchRank) return b.matchRank - a.matchRank;
149
347
  const ra = index.records[a.idx];
150
348
  const rb = index.records[b.idx];
151
- return ra.provinceNameTh.localeCompare(rb.provinceNameTh, "th") || ra.amphureNameTh.localeCompare(rb.amphureNameTh, "th") || ra.tambonNameTh.localeCompare(rb.tambonNameTh, "th");
349
+ return TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) || TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) || TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh);
152
350
  });
153
- return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
351
+ return window.slice(0, limit).map(({ idx }) => index.records[idx]);
352
+ }
353
+
354
+ // src/core/enumerate.ts
355
+ var thCollator = new Intl.Collator("th");
356
+ function byNameTh(a, b) {
357
+ return thCollator.compare(a.nameTh, b.nameTh);
358
+ }
359
+ function listProvinces(index) {
360
+ const seen = /* @__PURE__ */ new Map();
361
+ if (index.byProvince) {
362
+ for (const [provinceId, recordIndices] of index.byProvince) {
363
+ if (seen.has(provinceId) || recordIndices.length === 0) continue;
364
+ const r = index.records[recordIndices[0]];
365
+ seen.set(provinceId, { id: provinceId, nameTh: r.provinceNameTh, nameEn: r.provinceNameEn });
366
+ }
367
+ } else {
368
+ for (const r of index.records) {
369
+ if (!seen.has(r.provinceId)) {
370
+ seen.set(r.provinceId, { id: r.provinceId, nameTh: r.provinceNameTh, nameEn: r.provinceNameEn });
371
+ }
372
+ }
373
+ }
374
+ return Array.from(seen.values()).sort(byNameTh);
375
+ }
376
+ function listAmphures(index, provinceId) {
377
+ const seen = /* @__PURE__ */ new Map();
378
+ const addFrom = (r) => {
379
+ if (!seen.has(r.amphureId)) {
380
+ seen.set(r.amphureId, {
381
+ id: r.amphureId,
382
+ nameTh: r.amphureNameTh,
383
+ nameEn: r.amphureNameEn,
384
+ provinceId: r.provinceId
385
+ });
386
+ }
387
+ };
388
+ if (index.byProvince) {
389
+ const recordIndices = index.byProvince.get(provinceId);
390
+ if (!recordIndices) return [];
391
+ for (const idx of recordIndices) addFrom(index.records[idx]);
392
+ } else {
393
+ for (const r of index.records) {
394
+ if (r.provinceId === provinceId) addFrom(r);
395
+ }
396
+ }
397
+ return Array.from(seen.values()).sort(byNameTh);
398
+ }
399
+ function listTambons(index, amphureId) {
400
+ const toSummary = (r) => ({
401
+ id: r.tambonId,
402
+ nameTh: r.tambonNameTh,
403
+ nameEn: r.tambonNameEn,
404
+ amphureId: r.amphureId,
405
+ zipCode: r.zipCode
406
+ });
407
+ let result;
408
+ if (index.byAmphure) {
409
+ const recordIndices = index.byAmphure.get(amphureId);
410
+ if (!recordIndices) return [];
411
+ result = recordIndices.map((idx) => toSummary(index.records[idx]));
412
+ } else {
413
+ result = index.records.filter((r) => r.amphureId === amphureId).map(toSummary);
414
+ }
415
+ return result.sort(byNameTh);
154
416
  }
155
417
 
156
418
  // src/core/formatter.ts
157
- function formatThaiAddressSuggestion(record) {
419
+ function formatThaiAddressSuggestion(record, options) {
420
+ const labelTh = `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`;
421
+ const labelEn = `${record.tambonNameEn} > ${record.amphureNameEn} > ${record.provinceNameEn} ${record.zipCode}`;
158
422
  return {
159
423
  id: String(record.tambonId),
160
- label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,
424
+ label: options?.locale === "en" ? labelEn : labelTh,
425
+ labelTh,
426
+ labelEn,
161
427
  tambon: record.tambonNameTh,
162
428
  tambonEn: record.tambonNameEn,
163
429
  amphure: record.amphureNameTh,
@@ -186,10 +452,16 @@ function resolveThaiAddress(record) {
186
452
  };
187
453
  }
188
454
 
455
+ exports.applyRomanizationAliases = applyRomanizationAliases;
189
456
  exports.buildThaiAddressIndex = buildThaiAddressIndex;
190
457
  exports.formatThaiAddressSuggestion = formatThaiAddressSuggestion;
458
+ exports.listAmphures = listAmphures;
459
+ exports.listProvinces = listProvinces;
460
+ exports.listTambons = listTambons;
461
+ exports.lookupByZipCode = lookupByZipCode;
191
462
  exports.normalizeThaiAddressText = normalizeThaiAddressText;
192
463
  exports.resolveThaiAddress = resolveThaiAddress;
193
464
  exports.searchThaiAddress = searchThaiAddress;
465
+ exports.validateRawData = validateRawData;
194
466
  //# sourceMappingURL=index.cjs.map
195
467
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts"],"names":[],"mappings":";;;AAAA,IAAM,UAAA,GAAa,UAAA;AACnB,IAAM,gBAAA,GAAmB,gDAAA;AAElB,SAAS,yBAAyB,KAAA,EAAuB;AAC9D,EAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AACnB,EAAA,IAAI,IAAA,GAAO,MAAM,IAAA,EAAK;AACtB,EAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,gBAAA,EAAkB,EAAE,EAAE,IAAA,EAAK;AAC/C,EAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA;AAClC,EAAA,IAAA,GAAO,KAAK,WAAA,EAAY;AACxB,EAAA,OAAO,IAAA;AACT;;;ACPO,SAAS,gBAAgB,IAAA,EAA2B;AACzD,EAAA,OAAO,yBAAA,CAA0B,wBAAA,CAAyB,IAAI,CAAC,CAAA;AACjE;AAGO,SAAS,0BAA0B,IAAA,EAA2B;AACnE,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAC9B,EAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,IAAA,MAAA,CAAO,IAAI,IAAI,CAAA;AACf,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,IAAK,IAAA,CAAK,MAAA,GAAS,GAAG,CAAA,EAAA,EAAK;AACzC,IAAA,MAAA,CAAO,IAAI,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,CAAC,CAAC,CAAA;AAAA,EACjC;AACA,EAAA,OAAO,MAAA;AACT;;;AChBA,SAAS,WAAA,CAAY,GAAA,EAA+B,QAAA,EAAuB,GAAA,EAAmB;AAC5F,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAI,GAAA,GAAM,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AACzB,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,GAAA,uBAAU,GAAA,EAAI;AACd,MAAA,GAAA,CAAI,GAAA,CAAI,SAAS,GAAG,CAAA;AAAA,IACtB;AACA,IAAA,GAAA,CAAI,IAAI,GAAG,CAAA;AAAA,EACb;AACF;AAEA,SAAS,gBAAA,CAAiB,QAAgB,MAAA,EAA6B;AACrE,EAAA,MAAM,CAAA,uBAAQ,GAAA,EAAY;AAC1B,EAAA,KAAA,MAAW,KAAK,eAAA,CAAgB,MAAM,CAAA,EAAG,CAAA,CAAE,IAAI,CAAC,CAAA;AAChD,EAAA,KAAA,MAAW,KAAK,eAAA,CAAgB,MAAM,CAAA,EAAG,CAAA,CAAE,IAAI,CAAC,CAAA;AAChD,EAAA,OAAO,CAAA;AACT;AAEO,SAAS,qBAAA,CAAsB,MAAe,OAAA,EAA2C;AAC9F,EAAA,MAAM,EAAE,SAAA,EAAW,QAAA,EAAU,OAAA,EAAQ,GAAI,IAAA;AAGzC,EAAA,MAAM,UAAU,IAAI,GAAA;AAAA,IAClB,SAAA,CAAU,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,CAAA,CAAE,UAAU,CAAA,CAAE,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC;AAAA,GACzD;AACA,EAAA,MAAM,SAAS,IAAI,GAAA;AAAA,IACjB,QAAA,CAAS,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,CAAA,CAAE,UAAU,CAAA,CAAE,GAAA,CAAI,CAAA,CAAA,KAAK,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC;AAAA,GACxD;AAIA,EAAA,MAAM,YAAA,uBAAmB,GAAA,EAAyB;AAClD,EAAA,KAAA,MAAW,CAAC,EAAA,EAAI,IAAI,CAAA,IAAK,OAAA,EAAS;AAChC,IAAA,YAAA,CAAa,IAAI,EAAA,EAAI,gBAAA,CAAiB,KAAK,OAAA,EAAS,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,EACnE;AACA,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAyB;AACjD,EAAA,KAAA,MAAW,CAAC,EAAA,EAAI,GAAG,CAAA,IAAK,MAAA,EAAQ;AAC9B,IAAA,WAAA,CAAY,IAAI,EAAA,EAAI,gBAAA,CAAiB,IAAI,OAAA,EAAS,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,EAChE;AAEA,EAAA,MAAM,UAA+B,EAAC;AACtC,EAAA,MAAM,GAAA,uBAAU,GAAA,EAAyB;AACzC,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAsB;AAE3C,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,IAAI,OAAO,UAAA,EAAY;AAEvB,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,MAAA,CAAO,UAAU,CAAA;AAC5C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,IAAI,OAAA,EAAS,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,MAAmB,CAAA;AACvD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,WAAW,CAAA;AAChD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,IAAI,OAAA,EAAS,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,MAAmB,CAAA;AACvD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAA4B;AAAA,MAChC,YAAY,QAAA,CAAS,EAAA;AAAA,MACrB,gBAAgB,QAAA,CAAS,OAAA;AAAA,MACzB,gBAAgB,QAAA,CAAS,OAAA;AAAA,MACzB,WAAW,OAAA,CAAQ,EAAA;AAAA,MACnB,eAAe,OAAA,CAAQ,OAAA;AAAA,MACvB,eAAe,OAAA,CAAQ,OAAA;AAAA,MACvB,UAAU,MAAA,CAAO,EAAA;AAAA,MACjB,cAAc,MAAA,CAAO,OAAA;AAAA,MACrB,cAAc,MAAA,CAAO,OAAA;AAAA,MACrB,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,QAAQ;AAAA,KACjC;AAEA,IAAA,MAAM,MAAM,OAAA,CAAQ,MAAA;AACpB,IAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AAGnB,IAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,OAAO,CAAA;AAC5C,IAAA,IAAI,QAAA,EAAU,QAAA,CAAS,IAAA,CAAK,GAAG,CAAA;AAAA,kBACjB,GAAA,CAAI,MAAA,CAAO,OAAA,EAAS,CAAC,GAAG,CAAC,CAAA;AAGvC,IAAA,WAAA,CAAY,GAAA,EAAK,eAAA,CAAgB,MAAA,CAAO,YAAY,GAAG,GAAG,CAAA;AAC1D,IAAA,WAAA,CAAY,GAAA,EAAK,eAAA,CAAgB,MAAA,CAAO,YAAY,GAAG,GAAG,CAAA;AAC1D,IAAA,WAAA,CAAY,GAAA,EAAK,eAAA,CAAgB,MAAA,CAAO,OAAO,GAAG,GAAG,CAAA;AAErD,IAAA,WAAA,CAAY,KAAK,YAAA,CAAa,GAAA,CAAI,QAAA,CAAS,EAAE,GAAI,GAAG,CAAA;AACpD,IAAA,WAAA,CAAY,KAAK,WAAA,CAAY,GAAA,CAAI,OAAA,CAAQ,EAAE,GAAI,GAAG,CAAA;AAAA,EACpD;AAEA,EAAA,OAAO,EAAE,GAAA,EAAK,OAAA,EAAS,QAAA,EAAS;AAClC;;;ACzFA,IAAM,WAAA,GAAc,OAAA;AAEb,SAAS,iBAAA,CACd,KAAA,EACA,KAAA,EACA,OAAA,EACqB;AACrB,EAAA,MAAM,KAAA,GAAQ,SAAS,KAAA,IAAS,EAAA;AAChC,EAAA,MAAM,SAAA,GAAY,SAAS,SAAA,IAAa,GAAA;AAExC,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,KAAA,SAAc,EAAC;AAC9B,EAAA,MAAM,UAAA,GAAa,yBAAyB,KAAK,CAAA;AACjD,EAAA,IAAI,UAAA,CAAW,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AACrC,EAAA,IAAI,UAAA,CAAW,MAAA,GAAS,GAAA,EAAK,OAAO,EAAC;AAGrC,EAAA,IAAI,WAAA,CAAY,IAAA,CAAK,UAAU,CAAA,EAAG;AAChC,IAAA,IAAI,UAAA,CAAW,MAAA,GAAS,CAAA,EAAG,OAAO,EAAC;AACnC,IAAA,MAAM,UAA+B,EAAC;AACtC,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,OAAO,CAAA,IAAK,MAAM,QAAA,EAAU;AAC3C,MAAA,IAAI,GAAA,CAAI,UAAA,CAAW,UAAU,CAAA,EAAG;AAC9B,QAAA,KAAA,MAAW,OAAO,OAAA,EAAS,OAAA,CAAQ,KAAK,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAC,CAAA;AAAA,MAC5D;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AACrB,MAAA,IAAI,EAAE,OAAA,KAAY,UAAA,IAAc,CAAA,CAAE,OAAA,KAAY,YAAY,OAAO,EAAA;AACjE,MAAA,IAAI,EAAE,OAAA,KAAY,UAAA,IAAc,CAAA,CAAE,OAAA,KAAY,YAAY,OAAO,CAAA;AACjE,MAAA,OAAO,CAAA,CAAE,QAAQ,aAAA,CAAc,CAAA,CAAE,SAAS,MAAA,EAAW,EAAE,OAAA,EAAS,IAAA,EAAM,CAAA;AAAA,IACxE,CAAC,CAAA;AACD,IAAA,OAAO,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA;AAAA,EAC/B;AAMA,EAAA,IAAI,UAAA,CAAW,SAAS,CAAA,IAAK,KAAA,CAAM,MAAK,CAAE,MAAA,GAAS,CAAA,EAAG,OAAO,EAAC;AAE9D,EAAA,MAAM,aAAA,GAAgB,0BAA0B,UAAU,CAAA;AAG1D,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAoB;AACrC,EAAA,KAAA,MAAW,WAAW,aAAA,EAAe;AACnC,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,CAAC,UAAA,EAAY;AACjB,IAAA,KAAA,MAAW,OAAO,UAAA,EAAY;AAC5B,MAAA,IAAA,CAAK,IAAI,GAAA,EAAA,CAAM,IAAA,CAAK,IAAI,GAAG,CAAA,IAAK,KAAK,CAAC,CAAA;AAAA,IACxC;AAAA,EACF;AAEA,EAAA,MAAM,SAA2C,EAAC;AAClD,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,CAAA,IAAK,IAAA,EAAM;AAC/B,IAAA,MAAM,KAAA,GAAQ,QAAQ,aAAA,CAAc,IAAA;AACpC,IAAA,IAAI,SAAS,SAAA,EAAW;AACtB,MAAA,MAAA,CAAO,IAAA,CAAK,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA;AAAA,IAC5B;AAAA,EACF;AAEA,EAAA,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM;AACpB,IAAA,IAAI,EAAE,KAAA,KAAU,CAAA,CAAE,OAAO,OAAO,CAAA,CAAE,QAAQ,CAAA,CAAE,KAAA;AAC5C,IAAA,MAAM,EAAA,GAAK,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,GAAG,CAAA;AAC9B,IAAA,MAAM,EAAA,GAAK,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,GAAG,CAAA;AAC9B,IAAA,OACE,GAAG,cAAA,CAAe,aAAA,CAAc,GAAG,cAAA,EAAgB,IAAI,KACvD,EAAA,CAAG,aAAA,CAAc,cAAc,EAAA,CAAG,aAAA,EAAe,IAAI,CAAA,IACrD,EAAA,CAAG,aAAa,aAAA,CAAc,EAAA,CAAG,cAAc,IAAI,CAAA;AAAA,EAEvD,CAAC,CAAA;AAED,EAAA,OAAO,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,KAAK,CAAA,CAAE,GAAA,CAAI,CAAC,EAAE,GAAA,EAAI,KAAM,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAC,CAAA;AACnE;;;ACzEO,SAAS,4BAA4B,MAAA,EAAkD;AAC5F,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,MAAA,CAAO,MAAA,CAAO,QAAQ,CAAA;AAAA,IAC1B,KAAA,EAAO,CAAA,EAAG,MAAA,CAAO,YAAY,CAAA,GAAA,EAAM,MAAA,CAAO,aAAa,CAAA,GAAA,EAAM,MAAA,CAAO,cAAc,CAAA,CAAA,EAAI,MAAA,CAAO,OAAO,CAAA,CAAA;AAAA,IACpG,QAAQ,MAAA,CAAO,YAAA;AAAA,IACf,UAAU,MAAA,CAAO,YAAA;AAAA,IACjB,SAAS,MAAA,CAAO,aAAA;AAAA,IAChB,WAAW,MAAA,CAAO,aAAA;AAAA,IAClB,UAAU,MAAA,CAAO,cAAA;AAAA,IACjB,YAAY,MAAA,CAAO,cAAA;AAAA,IACnB,SAAS,MAAA,CAAO;AAAA,GAClB;AACF;;;ACZO,SAAS,mBAAmB,MAAA,EAAgD;AACjF,EAAA,OAAO;AAAA,IACL,QAAQ,MAAA,CAAO,YAAA;AAAA,IACf,UAAU,MAAA,CAAO,YAAA;AAAA,IACjB,SAAS,MAAA,CAAO,aAAA;AAAA,IAChB,WAAW,MAAA,CAAO,aAAA;AAAA,IAClB,UAAU,MAAA,CAAO,cAAA;AAAA,IACjB,YAAY,MAAA,CAAO,cAAA;AAAA,IACnB,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,aAAa,MAAA,CAAO,YAAA;AAAA,IACpB,eAAe,MAAA,CAAO,YAAA;AAAA,IACtB,UAAU,MAAA,CAAO,aAAA;AAAA,IACjB,YAAY,MAAA,CAAO,aAAA;AAAA,IACnB,YAAY,MAAA,CAAO;AAAA,GACrB;AACF","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 } from './trigrams'\nimport type { BuildIndexOptions, RawData, RawTambon, ThaiAddressRecord, TrigramIndex } from '../types'\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 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\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 // Tambon-specific fields (unique per record)\n addTrigrams(map, extractTrigrams(record.tambonNameTh), idx)\n addTrigrams(map, extractTrigrams(record.tambonNameEn), 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 }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\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 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 const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\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.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\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 }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\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"]}
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"]}