thaizip 0.5.1 → 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/README.md +90 -213
- package/dist/data.cjs +108 -34
- package/dist/data.d.cts +16 -0
- package/dist/data.d.ts +16 -0
- package/dist/data.js +108 -34
- package/dist/index.cjs +327 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +160 -20
- package/dist/index.d.ts +160 -20
- package/dist/index.js +322 -104
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +374 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +111 -0
- package/dist/react.d.ts +111 -0
- package/dist/react.js +372 -0
- package/dist/react.js.map +1 -0
- package/package.json +9 -4
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var react = require('react');
|
|
4
|
-
|
|
5
3
|
// src/core/normalizer.ts
|
|
6
|
-
var TONE_MARKS = /[
|
|
7
|
-
var ADDRESS_PREFIXES = /^(
|
|
4
|
+
var TONE_MARKS = /[่-๋็์]/g;
|
|
5
|
+
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
|
|
8
6
|
function normalizeThaiAddressText(input) {
|
|
9
7
|
if (!input) return "";
|
|
10
8
|
let text = input.trim();
|
|
@@ -32,7 +30,56 @@ function extractTrigramsNormalized(text) {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
// src/core/indexer.ts
|
|
35
|
-
function
|
|
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
|
+
}
|
|
63
|
+
function addTrigrams(map, trigrams, idx) {
|
|
64
|
+
for (const trigram of trigrams) {
|
|
65
|
+
let set = map.get(trigram);
|
|
66
|
+
if (!set) {
|
|
67
|
+
set = /* @__PURE__ */ new Set();
|
|
68
|
+
map.set(trigram, set);
|
|
69
|
+
}
|
|
70
|
+
set.add(idx);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function combinedTrigrams(nameTh, nameEn) {
|
|
74
|
+
const s = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const t of extractTrigrams(nameTh)) s.add(t);
|
|
76
|
+
for (const t of extractTrigrams(nameEn)) s.add(t);
|
|
77
|
+
return s;
|
|
78
|
+
}
|
|
79
|
+
function buildThaiAddressIndex(data, options) {
|
|
80
|
+
if (options?.validate !== false) {
|
|
81
|
+
validateRawData(data);
|
|
82
|
+
}
|
|
36
83
|
const { provinces, amphures, tambons } = data;
|
|
37
84
|
const provMap = new Map(
|
|
38
85
|
provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
|
|
@@ -40,20 +87,33 @@ function buildThaiAddressIndex(data) {
|
|
|
40
87
|
const ampMap = new Map(
|
|
41
88
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
42
89
|
);
|
|
90
|
+
const provTrigrams = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const [id, prov] of provMap) {
|
|
92
|
+
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
93
|
+
}
|
|
94
|
+
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const [id, amp] of ampMap) {
|
|
96
|
+
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
97
|
+
}
|
|
43
98
|
const records = [];
|
|
44
99
|
const map = /* @__PURE__ */ new Map();
|
|
45
100
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
101
|
+
const normTambon = [];
|
|
102
|
+
const normTambonEn = [];
|
|
103
|
+
const byProvince = /* @__PURE__ */ new Map();
|
|
104
|
+
const byAmphure = /* @__PURE__ */ new Map();
|
|
46
105
|
for (const tambon of tambons) {
|
|
47
106
|
if (tambon.deleted_at) continue;
|
|
48
107
|
const amphure = ampMap.get(tambon.amphure_id);
|
|
49
108
|
if (!amphure) {
|
|
50
|
-
|
|
51
|
-
`[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): amphure_id=${tambon.amphure_id} not found or deleted`
|
|
52
|
-
);
|
|
109
|
+
if (options?.onSkip) options.onSkip(tambon);
|
|
53
110
|
continue;
|
|
54
111
|
}
|
|
55
112
|
const province = provMap.get(amphure.province_id);
|
|
56
|
-
if (!province)
|
|
113
|
+
if (!province) {
|
|
114
|
+
if (options?.onSkip) options.onSkip(tambon);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
57
117
|
const record = {
|
|
58
118
|
provinceId: province.id,
|
|
59
119
|
provinceNameTh: province.name_th,
|
|
@@ -71,56 +131,195 @@ function buildThaiAddressIndex(data) {
|
|
|
71
131
|
const existing = zipIndex.get(record.zipCode);
|
|
72
132
|
if (existing) existing.push(idx);
|
|
73
133
|
else zipIndex.set(record.zipCode, [idx]);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
set.add(idx);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
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);
|
|
146
|
+
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
147
|
+
addTrigrams(map, provTrigrams.get(province.id), idx);
|
|
148
|
+
addTrigrams(map, ampTrigrams.get(amphure.id), idx);
|
|
93
149
|
}
|
|
94
|
-
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;
|
|
95
251
|
}
|
|
96
|
-
function
|
|
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;
|
|
97
260
|
}
|
|
98
261
|
|
|
99
262
|
// src/core/search.ts
|
|
100
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
|
+
}
|
|
101
306
|
function searchThaiAddress(index, query, options) {
|
|
102
307
|
const limit = options?.limit ?? 10;
|
|
103
308
|
const threshold = options?.threshold ?? 0.4;
|
|
104
309
|
if (!index || !query) return [];
|
|
310
|
+
if (query.length > 1e3) return [];
|
|
105
311
|
const normalized = normalizeThaiAddressText(query);
|
|
106
312
|
if (normalized.length === 0) return [];
|
|
107
313
|
if (normalized.length > 300) return [];
|
|
108
314
|
if (ZIP_CODE_RE.test(normalized)) {
|
|
109
315
|
if (normalized.length < 2) return [];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (zip.startsWith(normalized)) {
|
|
113
|
-
for (const idx of indices) matches.push(index.records[idx]);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
matches.sort((a, b) => {
|
|
117
|
-
if (a.zipCode === normalized && b.zipCode !== normalized) return -1;
|
|
118
|
-
if (b.zipCode === normalized && a.zipCode !== normalized) return 1;
|
|
119
|
-
return a.zipCode.localeCompare(b.zipCode);
|
|
316
|
+
return lookupByZipCode(index, normalized, {
|
|
317
|
+
zipLimit: options?.zipLimit ?? Infinity
|
|
120
318
|
});
|
|
121
|
-
return matches.slice(0, limit);
|
|
122
319
|
}
|
|
123
|
-
|
|
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);
|
|
124
323
|
const hits = /* @__PURE__ */ new Map();
|
|
125
324
|
for (const trigram of queryTrigrams) {
|
|
126
325
|
const candidates = index.map.get(trigram);
|
|
@@ -133,23 +332,98 @@ function searchThaiAddress(index, query, options) {
|
|
|
133
332
|
for (const [idx, count] of hits) {
|
|
134
333
|
const score = count / queryTrigrams.size;
|
|
135
334
|
if (score >= threshold) {
|
|
136
|
-
scored.push({ idx, score });
|
|
335
|
+
scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) });
|
|
137
336
|
}
|
|
138
337
|
}
|
|
139
338
|
scored.sort((a, b) => {
|
|
140
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;
|
|
141
347
|
const ra = index.records[a.idx];
|
|
142
348
|
const rb = index.records[b.idx];
|
|
143
|
-
return ra.provinceNameTh
|
|
349
|
+
return TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) || TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) || TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh);
|
|
350
|
+
});
|
|
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
|
|
144
406
|
});
|
|
145
|
-
|
|
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);
|
|
146
416
|
}
|
|
147
417
|
|
|
148
418
|
// src/core/formatter.ts
|
|
149
|
-
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}`;
|
|
150
422
|
return {
|
|
151
423
|
id: String(record.tambonId),
|
|
152
|
-
label:
|
|
424
|
+
label: options?.locale === "en" ? labelEn : labelTh,
|
|
425
|
+
labelTh,
|
|
426
|
+
labelEn,
|
|
153
427
|
tambon: record.tambonNameTh,
|
|
154
428
|
tambonEn: record.tambonNameEn,
|
|
155
429
|
amphure: record.amphureNameTh,
|
|
@@ -177,69 +451,17 @@ function resolveThaiAddress(record) {
|
|
|
177
451
|
postalCode: record.zipCode
|
|
178
452
|
};
|
|
179
453
|
}
|
|
180
|
-
function useThaiAddressAutocomplete(options) {
|
|
181
|
-
const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
|
|
182
|
-
const [query, setQueryState] = react.useState("");
|
|
183
|
-
const [suggestions, setSuggestions] = react.useState([]);
|
|
184
|
-
const timerRef = react.useRef(null);
|
|
185
|
-
const matchedRecordsMapRef = react.useRef(/* @__PURE__ */ new Map());
|
|
186
|
-
const setQuery = react.useCallback((value) => {
|
|
187
|
-
setQueryState(value);
|
|
188
|
-
}, []);
|
|
189
|
-
react.useEffect(() => {
|
|
190
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
191
|
-
if (query.length === 0) {
|
|
192
|
-
setSuggestions([]);
|
|
193
|
-
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
timerRef.current = setTimeout(() => {
|
|
197
|
-
const records = searchThaiAddress(index, query, { limit, threshold });
|
|
198
|
-
const formatted = records.map(formatThaiAddressSuggestion);
|
|
199
|
-
const recordMap = /* @__PURE__ */ new Map();
|
|
200
|
-
for (let i = 0; i < records.length; i++) {
|
|
201
|
-
recordMap.set(formatted[i].id, records[i]);
|
|
202
|
-
}
|
|
203
|
-
matchedRecordsMapRef.current = recordMap;
|
|
204
|
-
setSuggestions(formatted);
|
|
205
|
-
}, debounceMs);
|
|
206
|
-
return () => {
|
|
207
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
208
|
-
};
|
|
209
|
-
}, [query, index, limit, threshold, debounceMs]);
|
|
210
|
-
const selectSuggestion = react.useCallback(
|
|
211
|
-
(item) => {
|
|
212
|
-
const record = matchedRecordsMapRef.current.get(item.id);
|
|
213
|
-
if (!record) {
|
|
214
|
-
throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
|
|
215
|
-
}
|
|
216
|
-
setSuggestions([]);
|
|
217
|
-
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
218
|
-
return resolveThaiAddress(record);
|
|
219
|
-
},
|
|
220
|
-
[]
|
|
221
|
-
);
|
|
222
|
-
const clear = react.useCallback(() => {
|
|
223
|
-
setQueryState("");
|
|
224
|
-
setSuggestions([]);
|
|
225
|
-
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
226
|
-
}, []);
|
|
227
|
-
return {
|
|
228
|
-
query,
|
|
229
|
-
setQuery,
|
|
230
|
-
suggestions,
|
|
231
|
-
isOpen: query.length > 0 && suggestions.length > 0,
|
|
232
|
-
selectSuggestion,
|
|
233
|
-
clear
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
454
|
|
|
455
|
+
exports.applyRomanizationAliases = applyRomanizationAliases;
|
|
237
456
|
exports.buildThaiAddressIndex = buildThaiAddressIndex;
|
|
238
|
-
exports.clearDefaultIndex = clearDefaultIndex;
|
|
239
457
|
exports.formatThaiAddressSuggestion = formatThaiAddressSuggestion;
|
|
458
|
+
exports.listAmphures = listAmphures;
|
|
459
|
+
exports.listProvinces = listProvinces;
|
|
460
|
+
exports.listTambons = listTambons;
|
|
461
|
+
exports.lookupByZipCode = lookupByZipCode;
|
|
240
462
|
exports.normalizeThaiAddressText = normalizeThaiAddressText;
|
|
241
463
|
exports.resolveThaiAddress = resolveThaiAddress;
|
|
242
464
|
exports.searchThaiAddress = searchThaiAddress;
|
|
243
|
-
exports.
|
|
465
|
+
exports.validateRawData = validateRawData;
|
|
244
466
|
//# sourceMappingURL=index.cjs.map
|
|
245
467
|
//# sourceMappingURL=index.cjs.map
|