thaizip 0.6.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -263
- package/dist/data.cjs +98 -9
- package/dist/data.d.cts +25 -0
- package/dist/data.d.ts +25 -0
- package/dist/data.js +98 -9
- package/dist/index.cjs +380 -29
- package/dist/index.d.cts +187 -2
- package/dist/index.d.ts +187 -2
- package/dist/index.js +375 -30
- package/dist/react.cjs +267 -50
- package/dist/react.d.cts +49 -1
- package/dist/react.d.ts +49 -1
- package/dist/react.js +268 -51
- package/package.json +8 -5
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
var TONE_MARKS = /[่-๋็์]/g;
|
|
3
3
|
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
|
|
4
4
|
function normalizeThaiAddressText(input) {
|
|
5
|
-
if (
|
|
6
|
-
|
|
5
|
+
if (typeof input !== "string") {
|
|
6
|
+
throw new TypeError(
|
|
7
|
+
`[thaizip] normalizeThaiAddressText expected string, got ${input === null ? "null" : typeof input}`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
if (input.length === 0) return "";
|
|
11
|
+
let text = input.normalize("NFC").trim();
|
|
7
12
|
text = text.replace(ADDRESS_PREFIXES, "").trim();
|
|
8
13
|
text = text.replace(TONE_MARKS, "");
|
|
9
14
|
text = text.toLowerCase();
|
|
@@ -28,6 +33,69 @@ function extractTrigramsNormalized(text) {
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// src/core/indexer.ts
|
|
36
|
+
function typeErr(table, id, field, expected, value) {
|
|
37
|
+
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
38
|
+
}
|
|
39
|
+
function typeName(value) {
|
|
40
|
+
return value === null ? "null" : typeof value;
|
|
41
|
+
}
|
|
42
|
+
function validateRawData(data) {
|
|
43
|
+
if (data === null || typeof data !== "object") {
|
|
44
|
+
throw new TypeError(`[thaizip] validateRawData expected a RawData object, got ${typeName(data)}`);
|
|
45
|
+
}
|
|
46
|
+
const { provinces, amphures, tambons } = data;
|
|
47
|
+
if (!Array.isArray(provinces)) {
|
|
48
|
+
throw new TypeError(`[thaizip] RawData.provinces must be an array, got ${typeName(provinces)}`);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(amphures)) {
|
|
51
|
+
throw new TypeError(`[thaizip] RawData.amphures must be an array, got ${typeName(amphures)}`);
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(tambons)) {
|
|
54
|
+
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
55
|
+
}
|
|
56
|
+
const seenProvinceIds = /* @__PURE__ */ new Set();
|
|
57
|
+
for (let i = 0; i < provinces.length; i++) {
|
|
58
|
+
const p = provinces[i];
|
|
59
|
+
if (p === null || typeof p !== "object") {
|
|
60
|
+
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof p.id !== "number") throw typeErr("province", p.id, "id", "number", p.id);
|
|
63
|
+
if (seenProvinceIds.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
64
|
+
seenProvinceIds.add(p.id);
|
|
65
|
+
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
66
|
+
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
67
|
+
}
|
|
68
|
+
const seenAmphureIds = /* @__PURE__ */ new Set();
|
|
69
|
+
for (let i = 0; i < amphures.length; i++) {
|
|
70
|
+
const a = amphures[i];
|
|
71
|
+
if (a === null || typeof a !== "object") {
|
|
72
|
+
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof a.id !== "number") throw typeErr("amphure", a.id, "id", "number", a.id);
|
|
75
|
+
if (seenAmphureIds.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
76
|
+
seenAmphureIds.add(a.id);
|
|
77
|
+
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
78
|
+
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
79
|
+
if (typeof a.province_id !== "number") throw typeErr("amphure", a.id, "province_id", "number", a.province_id);
|
|
80
|
+
}
|
|
81
|
+
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
82
|
+
for (let i = 0; i < tambons.length; i++) {
|
|
83
|
+
const t = tambons[i];
|
|
84
|
+
if (t === null || typeof t !== "object") {
|
|
85
|
+
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof t.id !== "number") throw typeErr("tambon", t.id, "id", "number", t.id);
|
|
88
|
+
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
89
|
+
seenTambonIds.add(t.id);
|
|
90
|
+
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
91
|
+
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
92
|
+
const zipType = typeof t.zip_code;
|
|
93
|
+
if (zipType !== "string" && zipType !== "number") {
|
|
94
|
+
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
95
|
+
}
|
|
96
|
+
if (typeof t.amphure_id !== "number") throw typeErr("tambon", t.id, "amphure_id", "number", t.amphure_id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
31
99
|
function addTrigrams(map, trigrams, idx) {
|
|
32
100
|
for (const trigram of trigrams) {
|
|
33
101
|
let set = map.get(trigram);
|
|
@@ -45,6 +113,9 @@ function combinedTrigrams(nameTh, nameEn) {
|
|
|
45
113
|
return s;
|
|
46
114
|
}
|
|
47
115
|
function buildThaiAddressIndex(data, options) {
|
|
116
|
+
if (options?.validate !== false) {
|
|
117
|
+
validateRawData(data);
|
|
118
|
+
}
|
|
48
119
|
const { provinces, amphures, tambons } = data;
|
|
49
120
|
const provMap = new Map(
|
|
50
121
|
provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
|
|
@@ -63,6 +134,10 @@ function buildThaiAddressIndex(data, options) {
|
|
|
63
134
|
const records = [];
|
|
64
135
|
const map = /* @__PURE__ */ new Map();
|
|
65
136
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
137
|
+
const normTambon = [];
|
|
138
|
+
const normTambonEn = [];
|
|
139
|
+
const byProvince = /* @__PURE__ */ new Map();
|
|
140
|
+
const byAmphure = /* @__PURE__ */ new Map();
|
|
66
141
|
for (const tambon of tambons) {
|
|
67
142
|
if (tambon.deleted_at) continue;
|
|
68
143
|
const amphure = ampMap.get(tambon.amphure_id);
|
|
@@ -92,70 +167,340 @@ function buildThaiAddressIndex(data, options) {
|
|
|
92
167
|
const existing = zipIndex.get(record.zipCode);
|
|
93
168
|
if (existing) existing.push(idx);
|
|
94
169
|
else zipIndex.set(record.zipCode, [idx]);
|
|
95
|
-
|
|
96
|
-
|
|
170
|
+
const provList = byProvince.get(province.id);
|
|
171
|
+
if (provList) provList.push(idx);
|
|
172
|
+
else byProvince.set(province.id, [idx]);
|
|
173
|
+
const ampList = byAmphure.get(amphure.id);
|
|
174
|
+
if (ampList) ampList.push(idx);
|
|
175
|
+
else byAmphure.set(amphure.id, [idx]);
|
|
176
|
+
const normTh = normalizeThaiAddressText(record.tambonNameTh);
|
|
177
|
+
normTambon.push(normTh);
|
|
178
|
+
addTrigrams(map, extractTrigramsNormalized(normTh), idx);
|
|
179
|
+
const normEn = normalizeThaiAddressText(record.tambonNameEn);
|
|
180
|
+
normTambonEn.push(normEn);
|
|
181
|
+
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
97
182
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
98
183
|
addTrigrams(map, provTrigrams.get(province.id), idx);
|
|
99
184
|
addTrigrams(map, ampTrigrams.get(amphure.id), idx);
|
|
100
185
|
}
|
|
101
|
-
|
|
186
|
+
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
187
|
+
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
188
|
+
return { map, records, zipIndex, sortedZipKeys, sortedZipPostings, normTambon, normTambonEn, byProvince, byAmphure };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/core/romanize.ts
|
|
192
|
+
var ALIASES = /* @__PURE__ */ new Map([
|
|
193
|
+
// --- Bangkok khet (districts) — concatenated ("no space") forms ---------
|
|
194
|
+
["phranakhon", "phra nakhon"],
|
|
195
|
+
["nongchok", "nong chok"],
|
|
196
|
+
["bangrak", "bang rak"],
|
|
197
|
+
["bangkhen", "bang khen"],
|
|
198
|
+
["bangkapi", "bang kapi"],
|
|
199
|
+
["pathumwan", "pathum wan"],
|
|
200
|
+
["patumwan", "pathum wan"],
|
|
201
|
+
["pomprap", "pom prap sattru phai"],
|
|
202
|
+
["phrakhanong", "phra khanong"],
|
|
203
|
+
["minburi", "min buri"],
|
|
204
|
+
["latkrabang", "lat krabang"],
|
|
205
|
+
["ladkrabang", "lat krabang"],
|
|
206
|
+
["yannawa", "yan nawa"],
|
|
207
|
+
["phayathai", "phaya thai"],
|
|
208
|
+
["thonburi", "thon buri"],
|
|
209
|
+
["bangkokyai", "bangkok yai"],
|
|
210
|
+
["huaikhwang", "huai khwang"],
|
|
211
|
+
["huaykhwang", "huai khwang"],
|
|
212
|
+
["huaykwang", "huai khwang"],
|
|
213
|
+
["khlongsan", "khlong san"],
|
|
214
|
+
["klongsan", "khlong san"],
|
|
215
|
+
["talingchan", "taling chan"],
|
|
216
|
+
["bangkoknoi", "bangkok noi"],
|
|
217
|
+
["bangkhunthian", "bang khun thian"],
|
|
218
|
+
["phasicharoen", "phasi charoen"],
|
|
219
|
+
["nongkhaem", "nong khaem"],
|
|
220
|
+
["ratburana", "rat burana"],
|
|
221
|
+
["bangphlat", "bang phlat"],
|
|
222
|
+
["dindaeng", "din daeng"],
|
|
223
|
+
["buengkum", "bueng kum"],
|
|
224
|
+
["bangsue", "bang sue"],
|
|
225
|
+
["bangkholaem", "bang kho laem"],
|
|
226
|
+
["khlongtoei", "khlong toei"],
|
|
227
|
+
["klongtoei", "khlong toei"],
|
|
228
|
+
["klongtoey", "khlong toei"],
|
|
229
|
+
["khlongtoey", "khlong toei"],
|
|
230
|
+
["suanluang", "suan luang"],
|
|
231
|
+
["chomthong", "chom thong"],
|
|
232
|
+
["donmueang", "don mueang"],
|
|
233
|
+
["donmuang", "don mueang"],
|
|
234
|
+
["latphrao", "lat phrao"],
|
|
235
|
+
["ladprao", "lat phrao"],
|
|
236
|
+
["lardprao", "lat phrao"],
|
|
237
|
+
["lat prao", "lat phrao"],
|
|
238
|
+
["bangkhae", "bang khae"],
|
|
239
|
+
["laksi", "lak si"],
|
|
240
|
+
["saimai", "sai mai"],
|
|
241
|
+
["khannayao", "khan na yao"],
|
|
242
|
+
["saphansung", "saphan sung"],
|
|
243
|
+
["wangthonglang", "wang thonglang"],
|
|
244
|
+
["khlongsamwa", "khlong sam wa"],
|
|
245
|
+
["klongsamwa", "khlong sam wa"],
|
|
246
|
+
["bangna", "bang na"],
|
|
247
|
+
["thawiwatthana", "thawi watthana"],
|
|
248
|
+
["thungkhru", "thung khru"],
|
|
249
|
+
["bangbon", "bang bon"],
|
|
250
|
+
// --- Bangkok area / street nicknames with a common misspelling ----------
|
|
251
|
+
["sathorn", "sathon"],
|
|
252
|
+
["wattana", "watthana"],
|
|
253
|
+
["silom", "si lom"],
|
|
254
|
+
// --- Provinces and other high-traffic areas ------------------------------
|
|
255
|
+
["krungthep", "bangkok"],
|
|
256
|
+
["korat", "nakhon ratchasima"],
|
|
257
|
+
["nakornratchasima", "nakhon ratchasima"],
|
|
258
|
+
["chiangmai", "chiang mai"],
|
|
259
|
+
["chiangrai", "chiang rai"],
|
|
260
|
+
["pathumthani", "pathum thani"],
|
|
261
|
+
["patumthani", "pathum thani"],
|
|
262
|
+
["samutprakan", "samut prakan"],
|
|
263
|
+
["samutsakhon", "samut sakhon"],
|
|
264
|
+
["samutsongkhram", "samut songkhram"],
|
|
265
|
+
["nakhonpathom", "nakhon pathom"],
|
|
266
|
+
["nakhonsawan", "nakhon sawan"],
|
|
267
|
+
["nakhonsithammarat", "nakhon si thammarat"],
|
|
268
|
+
["prachuapkhirikhan", "prachuap khiri khan"],
|
|
269
|
+
["huahin", "hua hin"],
|
|
270
|
+
["hatyai", "hat yai"],
|
|
271
|
+
["hadyai", "hat yai"],
|
|
272
|
+
["had yai", "hat yai"],
|
|
273
|
+
["lopburi", "loburi"],
|
|
274
|
+
["lop buri", "loburi"],
|
|
275
|
+
["buengkan", "buogkan"],
|
|
276
|
+
["bungkan", "buogkan"],
|
|
277
|
+
["bung kan", "buogkan"],
|
|
278
|
+
["udonthani", "udon thani"],
|
|
279
|
+
["khonkaen", "khon kaen"],
|
|
280
|
+
["suphanburi", "suphan buri"],
|
|
281
|
+
["nongkhai", "nong khai"],
|
|
282
|
+
["buriram", "buri ram"]
|
|
283
|
+
]);
|
|
284
|
+
var NOISE_WORDS_RE = /\b(?:sub[- ]?district|district|province|changwat|amphoe|amphur)\b/g;
|
|
285
|
+
var WHITESPACE_RE = /\s+/g;
|
|
286
|
+
function cleanup(text) {
|
|
287
|
+
const stripped = text.replace(NOISE_WORDS_RE, " ").replace(WHITESPACE_RE, " ").trim();
|
|
288
|
+
return stripped.length > 0 ? stripped : text;
|
|
289
|
+
}
|
|
290
|
+
function applyRomanizationAliases(normalized) {
|
|
291
|
+
if (!normalized) return normalized;
|
|
292
|
+
const direct = ALIASES.get(normalized);
|
|
293
|
+
if (direct !== void 0) return direct;
|
|
294
|
+
const cleaned = cleanup(normalized);
|
|
295
|
+
if (cleaned === normalized) return normalized;
|
|
296
|
+
const aliasedCleaned = ALIASES.get(cleaned);
|
|
297
|
+
return aliasedCleaned !== void 0 ? aliasedCleaned : cleaned;
|
|
102
298
|
}
|
|
103
299
|
|
|
104
300
|
// src/core/search.ts
|
|
105
301
|
var ZIP_CODE_RE = /^\d+$/;
|
|
302
|
+
var HAS_LATIN_RE = /[a-z]/i;
|
|
303
|
+
var TH_COLLATOR = new Intl.Collator("th");
|
|
304
|
+
function compareZip(a, b) {
|
|
305
|
+
if (a < b) return -1;
|
|
306
|
+
if (a > b) return 1;
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
function sortZipMatches(matches, exactZip) {
|
|
310
|
+
matches.sort((a, b) => {
|
|
311
|
+
if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1;
|
|
312
|
+
if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1;
|
|
313
|
+
return compareZip(a.zipCode, b.zipCode);
|
|
314
|
+
});
|
|
315
|
+
return matches;
|
|
316
|
+
}
|
|
317
|
+
function normalizeLimit(value) {
|
|
318
|
+
if (Number.isNaN(value) || value === -Infinity) return 0;
|
|
319
|
+
if (value === Infinity) return value;
|
|
320
|
+
return Math.max(0, Math.floor(value));
|
|
321
|
+
}
|
|
322
|
+
function lookupByZipCode(index, zip, options) {
|
|
323
|
+
if (!index || !index.zipIndex || !index.records || typeof zip !== "string") return [];
|
|
324
|
+
const normalized = zip.trim();
|
|
325
|
+
if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return [];
|
|
326
|
+
const zipLimit = normalizeLimit(options?.zipLimit ?? Infinity);
|
|
327
|
+
const matches = [];
|
|
328
|
+
const keys = index.sortedZipKeys;
|
|
329
|
+
const postings = index.sortedZipPostings;
|
|
330
|
+
if (keys !== void 0 && postings !== void 0) {
|
|
331
|
+
let lo = 0;
|
|
332
|
+
let hi = keys.length;
|
|
333
|
+
while (lo < hi) {
|
|
334
|
+
const mid = lo + hi >> 1;
|
|
335
|
+
if (keys[mid] < normalized) lo = mid + 1;
|
|
336
|
+
else hi = mid;
|
|
337
|
+
}
|
|
338
|
+
for (let i = lo; i < keys.length && keys[i].startsWith(normalized); i++) {
|
|
339
|
+
const indices = postings[i];
|
|
340
|
+
for (let j = 0; j < indices.length; j++) matches.push(index.records[indices[j]]);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
for (const [z, indices] of index.zipIndex) {
|
|
344
|
+
if (z.startsWith(normalized)) {
|
|
345
|
+
for (const idx of indices) matches.push(index.records[idx]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
sortZipMatches(matches, normalized);
|
|
349
|
+
}
|
|
350
|
+
return matches.slice(0, zipLimit).map((record) => ({ ...record }));
|
|
351
|
+
}
|
|
352
|
+
function rankAgainstName(name, query) {
|
|
353
|
+
if (name === void 0 || name.length === 0) return 0;
|
|
354
|
+
if (name === query) return 3;
|
|
355
|
+
if (name.startsWith(query)) return 2;
|
|
356
|
+
if (name.includes(query)) return 1;
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
function computeMatchRank(index, idx, query) {
|
|
360
|
+
const th = rankAgainstName(index.normTambon?.[idx], query);
|
|
361
|
+
if (th === 3) return 3;
|
|
362
|
+
const en = rankAgainstName(index.normTambonEn?.[idx], query);
|
|
363
|
+
return en > th ? en : th;
|
|
364
|
+
}
|
|
365
|
+
var scratchByIndex = /* @__PURE__ */ new WeakMap();
|
|
366
|
+
function getScratch(index) {
|
|
367
|
+
const n = index.records.length;
|
|
368
|
+
let s = scratchByIndex.get(index);
|
|
369
|
+
if (s === void 0 || s.counts.length < n) {
|
|
370
|
+
s = { counts: new Uint32Array(n), touched: new Int32Array(n) };
|
|
371
|
+
scratchByIndex.set(index, s);
|
|
372
|
+
}
|
|
373
|
+
return s;
|
|
374
|
+
}
|
|
106
375
|
function searchThaiAddress(index, query, options) {
|
|
107
|
-
const limit = options?.limit ?? 10;
|
|
376
|
+
const limit = normalizeLimit(options?.limit ?? 10);
|
|
108
377
|
const threshold = options?.threshold ?? 0.4;
|
|
109
|
-
if (!index || !query) return [];
|
|
378
|
+
if (!index || !index.map || !index.records || typeof query !== "string") return [];
|
|
379
|
+
if (query.length > 1e3) return [];
|
|
110
380
|
const normalized = normalizeThaiAddressText(query);
|
|
111
381
|
if (normalized.length === 0) return [];
|
|
112
382
|
if (normalized.length > 300) return [];
|
|
113
383
|
if (ZIP_CODE_RE.test(normalized)) {
|
|
114
384
|
if (normalized.length < 2) return [];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (zip.startsWith(normalized)) {
|
|
118
|
-
for (const idx of indices) matches.push(index.records[idx]);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
matches.sort((a, b) => {
|
|
122
|
-
if (a.zipCode === normalized && b.zipCode !== normalized) return -1;
|
|
123
|
-
if (b.zipCode === normalized && a.zipCode !== normalized) return 1;
|
|
124
|
-
return a.zipCode.localeCompare(b.zipCode, void 0, { numeric: true });
|
|
385
|
+
return lookupByZipCode(index, normalized, {
|
|
386
|
+
zipLimit: options?.zipLimit ?? Infinity
|
|
125
387
|
});
|
|
126
|
-
return matches.slice(0, limit);
|
|
127
388
|
}
|
|
128
|
-
if (normalized.length < 3
|
|
129
|
-
const
|
|
130
|
-
const
|
|
389
|
+
if (normalized.length < 3) return [];
|
|
390
|
+
const searchText = options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized) ? applyRomanizationAliases(normalized) : normalized;
|
|
391
|
+
const queryTrigrams = extractTrigramsNormalized(searchText);
|
|
392
|
+
const { counts, touched } = getScratch(index);
|
|
393
|
+
let touchedLen = 0;
|
|
131
394
|
for (const trigram of queryTrigrams) {
|
|
132
395
|
const candidates = index.map.get(trigram);
|
|
133
396
|
if (!candidates) continue;
|
|
134
397
|
for (const idx of candidates) {
|
|
135
|
-
|
|
398
|
+
if (counts[idx] === 0) touched[touchedLen++] = idx;
|
|
399
|
+
counts[idx]++;
|
|
136
400
|
}
|
|
137
401
|
}
|
|
138
402
|
const scored = [];
|
|
139
|
-
|
|
140
|
-
|
|
403
|
+
const totalTrigrams = queryTrigrams.size;
|
|
404
|
+
for (let i = 0; i < touchedLen; i++) {
|
|
405
|
+
const idx = touched[i];
|
|
406
|
+
const score = counts[idx] / totalTrigrams;
|
|
407
|
+
counts[idx] = 0;
|
|
141
408
|
if (score >= threshold) {
|
|
142
|
-
scored.push({ idx, score });
|
|
409
|
+
scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) });
|
|
143
410
|
}
|
|
144
411
|
}
|
|
145
412
|
scored.sort((a, b) => {
|
|
146
413
|
if (b.score !== a.score) return b.score - a.score;
|
|
414
|
+
return b.matchRank - a.matchRank;
|
|
415
|
+
});
|
|
416
|
+
const windowSize = Math.max(limit * 4, 50);
|
|
417
|
+
const window = scored.slice(0, windowSize);
|
|
418
|
+
window.sort((a, b) => {
|
|
419
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
420
|
+
if (b.matchRank !== a.matchRank) return b.matchRank - a.matchRank;
|
|
147
421
|
const ra = index.records[a.idx];
|
|
148
422
|
const rb = index.records[b.idx];
|
|
149
|
-
return ra.provinceNameTh
|
|
423
|
+
return TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) || TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) || TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh);
|
|
424
|
+
});
|
|
425
|
+
return window.slice(0, limit).map(({ idx }) => ({ ...index.records[idx] }));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/core/enumerate.ts
|
|
429
|
+
var thCollator = new Intl.Collator("th");
|
|
430
|
+
function byNameTh(a, b) {
|
|
431
|
+
return thCollator.compare(a.nameTh, b.nameTh);
|
|
432
|
+
}
|
|
433
|
+
function listProvinces(index) {
|
|
434
|
+
if (!index || !index.records) return [];
|
|
435
|
+
const seen = /* @__PURE__ */ new Map();
|
|
436
|
+
if (index.byProvince) {
|
|
437
|
+
for (const [provinceId, recordIndices] of index.byProvince) {
|
|
438
|
+
if (seen.has(provinceId) || recordIndices.length === 0) continue;
|
|
439
|
+
const r = index.records[recordIndices[0]];
|
|
440
|
+
seen.set(provinceId, { id: provinceId, nameTh: r.provinceNameTh, nameEn: r.provinceNameEn });
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
for (const r of index.records) {
|
|
444
|
+
if (!seen.has(r.provinceId)) {
|
|
445
|
+
seen.set(r.provinceId, { id: r.provinceId, nameTh: r.provinceNameTh, nameEn: r.provinceNameEn });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return Array.from(seen.values()).sort(byNameTh);
|
|
450
|
+
}
|
|
451
|
+
function listAmphures(index, provinceId) {
|
|
452
|
+
if (!index || !index.records) return [];
|
|
453
|
+
const seen = /* @__PURE__ */ new Map();
|
|
454
|
+
const addFrom = (r) => {
|
|
455
|
+
if (!seen.has(r.amphureId)) {
|
|
456
|
+
seen.set(r.amphureId, {
|
|
457
|
+
id: r.amphureId,
|
|
458
|
+
nameTh: r.amphureNameTh,
|
|
459
|
+
nameEn: r.amphureNameEn,
|
|
460
|
+
provinceId: r.provinceId
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
if (index.byProvince) {
|
|
465
|
+
const recordIndices = index.byProvince.get(provinceId);
|
|
466
|
+
if (!recordIndices) return [];
|
|
467
|
+
for (const idx of recordIndices) addFrom(index.records[idx]);
|
|
468
|
+
} else {
|
|
469
|
+
for (const r of index.records) {
|
|
470
|
+
if (r.provinceId === provinceId) addFrom(r);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return Array.from(seen.values()).sort(byNameTh);
|
|
474
|
+
}
|
|
475
|
+
function listTambons(index, amphureId) {
|
|
476
|
+
if (!index || !index.records) return [];
|
|
477
|
+
const toSummary = (r) => ({
|
|
478
|
+
id: r.tambonId,
|
|
479
|
+
nameTh: r.tambonNameTh,
|
|
480
|
+
nameEn: r.tambonNameEn,
|
|
481
|
+
amphureId: r.amphureId,
|
|
482
|
+
zipCode: r.zipCode
|
|
150
483
|
});
|
|
151
|
-
|
|
484
|
+
let result;
|
|
485
|
+
if (index.byAmphure) {
|
|
486
|
+
const recordIndices = index.byAmphure.get(amphureId);
|
|
487
|
+
if (!recordIndices) return [];
|
|
488
|
+
result = recordIndices.map((idx) => toSummary(index.records[idx]));
|
|
489
|
+
} else {
|
|
490
|
+
result = index.records.filter((r) => r.amphureId === amphureId).map(toSummary);
|
|
491
|
+
}
|
|
492
|
+
return result.sort(byNameTh);
|
|
152
493
|
}
|
|
153
494
|
|
|
154
495
|
// src/core/formatter.ts
|
|
155
|
-
function formatThaiAddressSuggestion(record) {
|
|
496
|
+
function formatThaiAddressSuggestion(record, options) {
|
|
497
|
+
const labelTh = `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`;
|
|
498
|
+
const labelEn = `${record.tambonNameEn} > ${record.amphureNameEn} > ${record.provinceNameEn} ${record.zipCode}`;
|
|
156
499
|
return {
|
|
157
500
|
id: String(record.tambonId),
|
|
158
|
-
label:
|
|
501
|
+
label: options?.locale === "en" ? labelEn : labelTh,
|
|
502
|
+
labelTh,
|
|
503
|
+
labelEn,
|
|
159
504
|
tambon: record.tambonNameTh,
|
|
160
505
|
tambonEn: record.tambonNameEn,
|
|
161
506
|
amphure: record.amphureNameTh,
|
|
@@ -184,6 +529,6 @@ function resolveThaiAddress(record) {
|
|
|
184
529
|
};
|
|
185
530
|
}
|
|
186
531
|
|
|
187
|
-
export { buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress };
|
|
532
|
+
export { applyRomanizationAliases, buildThaiAddressIndex, formatThaiAddressSuggestion, listAmphures, listProvinces, listTambons, lookupByZipCode, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, validateRawData };
|
|
188
533
|
//# sourceMappingURL=index.js.map
|
|
189
534
|
//# sourceMappingURL=index.js.map
|