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