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