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/dist/react.js CHANGED
@@ -1,13 +1,17 @@
1
- import { useRef, useState, useEffect, useCallback } from 'react';
2
-
3
- // src/react/useThaiAddressAutocomplete.ts
1
+ "use client";
2
+ import { useRef, useState, useCallback, useEffect } from 'react';
4
3
 
5
4
  // src/core/normalizer.ts
6
5
  var TONE_MARKS = /[่-๋็์]/g;
7
6
  var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
8
7
  function normalizeThaiAddressText(input) {
9
- if (!input) return "";
10
- let text = input.trim();
8
+ if (typeof input !== "string") {
9
+ throw new TypeError(
10
+ `[thaizip] normalizeThaiAddressText expected string, got ${input === null ? "null" : typeof input}`
11
+ );
12
+ }
13
+ if (input.length === 0) return "";
14
+ let text = input.normalize("NFC").trim();
11
15
  text = text.replace(ADDRESS_PREFIXES, "").trim();
12
16
  text = text.replace(TONE_MARKS, "");
13
17
  text = text.toLowerCase();
@@ -28,61 +32,252 @@ function extractTrigramsNormalized(text) {
28
32
  return result;
29
33
  }
30
34
 
35
+ // src/core/romanize.ts
36
+ var ALIASES = /* @__PURE__ */ new Map([
37
+ // --- Bangkok khet (districts) — concatenated ("no space") forms ---------
38
+ ["phranakhon", "phra nakhon"],
39
+ ["nongchok", "nong chok"],
40
+ ["bangrak", "bang rak"],
41
+ ["bangkhen", "bang khen"],
42
+ ["bangkapi", "bang kapi"],
43
+ ["pathumwan", "pathum wan"],
44
+ ["patumwan", "pathum wan"],
45
+ ["pomprap", "pom prap sattru phai"],
46
+ ["phrakhanong", "phra khanong"],
47
+ ["minburi", "min buri"],
48
+ ["latkrabang", "lat krabang"],
49
+ ["ladkrabang", "lat krabang"],
50
+ ["yannawa", "yan nawa"],
51
+ ["phayathai", "phaya thai"],
52
+ ["thonburi", "thon buri"],
53
+ ["bangkokyai", "bangkok yai"],
54
+ ["huaikhwang", "huai khwang"],
55
+ ["huaykhwang", "huai khwang"],
56
+ ["huaykwang", "huai khwang"],
57
+ ["khlongsan", "khlong san"],
58
+ ["klongsan", "khlong san"],
59
+ ["talingchan", "taling chan"],
60
+ ["bangkoknoi", "bangkok noi"],
61
+ ["bangkhunthian", "bang khun thian"],
62
+ ["phasicharoen", "phasi charoen"],
63
+ ["nongkhaem", "nong khaem"],
64
+ ["ratburana", "rat burana"],
65
+ ["bangphlat", "bang phlat"],
66
+ ["dindaeng", "din daeng"],
67
+ ["buengkum", "bueng kum"],
68
+ ["bangsue", "bang sue"],
69
+ ["bangkholaem", "bang kho laem"],
70
+ ["khlongtoei", "khlong toei"],
71
+ ["klongtoei", "khlong toei"],
72
+ ["klongtoey", "khlong toei"],
73
+ ["khlongtoey", "khlong toei"],
74
+ ["suanluang", "suan luang"],
75
+ ["chomthong", "chom thong"],
76
+ ["donmueang", "don mueang"],
77
+ ["donmuang", "don mueang"],
78
+ ["latphrao", "lat phrao"],
79
+ ["ladprao", "lat phrao"],
80
+ ["lardprao", "lat phrao"],
81
+ ["lat prao", "lat phrao"],
82
+ ["bangkhae", "bang khae"],
83
+ ["laksi", "lak si"],
84
+ ["saimai", "sai mai"],
85
+ ["khannayao", "khan na yao"],
86
+ ["saphansung", "saphan sung"],
87
+ ["wangthonglang", "wang thonglang"],
88
+ ["khlongsamwa", "khlong sam wa"],
89
+ ["klongsamwa", "khlong sam wa"],
90
+ ["bangna", "bang na"],
91
+ ["thawiwatthana", "thawi watthana"],
92
+ ["thungkhru", "thung khru"],
93
+ ["bangbon", "bang bon"],
94
+ // --- Bangkok area / street nicknames with a common misspelling ----------
95
+ ["sathorn", "sathon"],
96
+ ["wattana", "watthana"],
97
+ ["silom", "si lom"],
98
+ // --- Provinces and other high-traffic areas ------------------------------
99
+ ["krungthep", "bangkok"],
100
+ ["korat", "nakhon ratchasima"],
101
+ ["nakornratchasima", "nakhon ratchasima"],
102
+ ["chiangmai", "chiang mai"],
103
+ ["chiangrai", "chiang rai"],
104
+ ["pathumthani", "pathum thani"],
105
+ ["patumthani", "pathum thani"],
106
+ ["samutprakan", "samut prakan"],
107
+ ["samutsakhon", "samut sakhon"],
108
+ ["samutsongkhram", "samut songkhram"],
109
+ ["nakhonpathom", "nakhon pathom"],
110
+ ["nakhonsawan", "nakhon sawan"],
111
+ ["nakhonsithammarat", "nakhon si thammarat"],
112
+ ["prachuapkhirikhan", "prachuap khiri khan"],
113
+ ["huahin", "hua hin"],
114
+ ["hatyai", "hat yai"],
115
+ ["hadyai", "hat yai"],
116
+ ["had yai", "hat yai"],
117
+ ["lopburi", "loburi"],
118
+ ["lop buri", "loburi"],
119
+ ["buengkan", "buogkan"],
120
+ ["bungkan", "buogkan"],
121
+ ["bung kan", "buogkan"],
122
+ ["udonthani", "udon thani"],
123
+ ["khonkaen", "khon kaen"],
124
+ ["suphanburi", "suphan buri"],
125
+ ["nongkhai", "nong khai"],
126
+ ["buriram", "buri ram"]
127
+ ]);
128
+ var NOISE_WORDS_RE = /\b(?:sub[- ]?district|district|province|changwat|amphoe|amphur)\b/g;
129
+ var WHITESPACE_RE = /\s+/g;
130
+ function cleanup(text) {
131
+ const stripped = text.replace(NOISE_WORDS_RE, " ").replace(WHITESPACE_RE, " ").trim();
132
+ return stripped.length > 0 ? stripped : text;
133
+ }
134
+ function applyRomanizationAliases(normalized) {
135
+ if (!normalized) return normalized;
136
+ const direct = ALIASES.get(normalized);
137
+ if (direct !== void 0) return direct;
138
+ const cleaned = cleanup(normalized);
139
+ if (cleaned === normalized) return normalized;
140
+ const aliasedCleaned = ALIASES.get(cleaned);
141
+ return aliasedCleaned !== void 0 ? aliasedCleaned : cleaned;
142
+ }
143
+
31
144
  // src/core/search.ts
32
145
  var ZIP_CODE_RE = /^\d+$/;
146
+ var HAS_LATIN_RE = /[a-z]/i;
147
+ var TH_COLLATOR = new Intl.Collator("th");
148
+ function compareZip(a, b) {
149
+ if (a < b) return -1;
150
+ if (a > b) return 1;
151
+ return 0;
152
+ }
153
+ function sortZipMatches(matches, exactZip) {
154
+ matches.sort((a, b) => {
155
+ if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1;
156
+ if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1;
157
+ return compareZip(a.zipCode, b.zipCode);
158
+ });
159
+ return matches;
160
+ }
161
+ function normalizeLimit(value) {
162
+ if (Number.isNaN(value) || value === -Infinity) return 0;
163
+ if (value === Infinity) return value;
164
+ return Math.max(0, Math.floor(value));
165
+ }
166
+ function lookupByZipCode(index, zip, options) {
167
+ if (!index || !index.zipIndex || !index.records || typeof zip !== "string") return [];
168
+ const normalized = zip.trim();
169
+ if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return [];
170
+ const zipLimit = normalizeLimit(options?.zipLimit ?? Infinity);
171
+ const matches = [];
172
+ const keys = index.sortedZipKeys;
173
+ const postings = index.sortedZipPostings;
174
+ if (keys !== void 0 && postings !== void 0) {
175
+ let lo = 0;
176
+ let hi = keys.length;
177
+ while (lo < hi) {
178
+ const mid = lo + hi >> 1;
179
+ if (keys[mid] < normalized) lo = mid + 1;
180
+ else hi = mid;
181
+ }
182
+ for (let i = lo; i < keys.length && keys[i].startsWith(normalized); i++) {
183
+ const indices = postings[i];
184
+ for (let j = 0; j < indices.length; j++) matches.push(index.records[indices[j]]);
185
+ }
186
+ } else {
187
+ for (const [z, indices] of index.zipIndex) {
188
+ if (z.startsWith(normalized)) {
189
+ for (const idx of indices) matches.push(index.records[idx]);
190
+ }
191
+ }
192
+ sortZipMatches(matches, normalized);
193
+ }
194
+ return matches.slice(0, zipLimit).map((record) => ({ ...record }));
195
+ }
196
+ function rankAgainstName(name, query) {
197
+ if (name === void 0 || name.length === 0) return 0;
198
+ if (name === query) return 3;
199
+ if (name.startsWith(query)) return 2;
200
+ if (name.includes(query)) return 1;
201
+ return 0;
202
+ }
203
+ function computeMatchRank(index, idx, query) {
204
+ const th = rankAgainstName(index.normTambon?.[idx], query);
205
+ if (th === 3) return 3;
206
+ const en = rankAgainstName(index.normTambonEn?.[idx], query);
207
+ return en > th ? en : th;
208
+ }
209
+ var scratchByIndex = /* @__PURE__ */ new WeakMap();
210
+ function getScratch(index) {
211
+ const n = index.records.length;
212
+ let s = scratchByIndex.get(index);
213
+ if (s === void 0 || s.counts.length < n) {
214
+ s = { counts: new Uint32Array(n), touched: new Int32Array(n) };
215
+ scratchByIndex.set(index, s);
216
+ }
217
+ return s;
218
+ }
33
219
  function searchThaiAddress(index, query, options) {
34
- const limit = options?.limit ?? 10;
220
+ const limit = normalizeLimit(options?.limit ?? 10);
35
221
  const threshold = options?.threshold ?? 0.4;
36
- if (!index || !query) return [];
222
+ if (!index || !index.map || !index.records || typeof query !== "string") return [];
223
+ if (query.length > 1e3) return [];
37
224
  const normalized = normalizeThaiAddressText(query);
38
225
  if (normalized.length === 0) return [];
39
226
  if (normalized.length > 300) return [];
40
227
  if (ZIP_CODE_RE.test(normalized)) {
41
228
  if (normalized.length < 2) return [];
42
- const matches = [];
43
- for (const [zip, indices] of index.zipIndex) {
44
- if (zip.startsWith(normalized)) {
45
- for (const idx of indices) matches.push(index.records[idx]);
46
- }
47
- }
48
- matches.sort((a, b) => {
49
- if (a.zipCode === normalized && b.zipCode !== normalized) return -1;
50
- if (b.zipCode === normalized && a.zipCode !== normalized) return 1;
51
- return a.zipCode.localeCompare(b.zipCode, void 0, { numeric: true });
229
+ return lookupByZipCode(index, normalized, {
230
+ zipLimit: options?.zipLimit ?? Infinity
52
231
  });
53
- return matches.slice(0, limit);
54
232
  }
55
- if (normalized.length < 3 && query.trim().length < 3) return [];
56
- const queryTrigrams = extractTrigramsNormalized(normalized);
57
- const hits = /* @__PURE__ */ new Map();
233
+ if (normalized.length < 3) return [];
234
+ const searchText = options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized) ? applyRomanizationAliases(normalized) : normalized;
235
+ const queryTrigrams = extractTrigramsNormalized(searchText);
236
+ const { counts, touched } = getScratch(index);
237
+ let touchedLen = 0;
58
238
  for (const trigram of queryTrigrams) {
59
239
  const candidates = index.map.get(trigram);
60
240
  if (!candidates) continue;
61
241
  for (const idx of candidates) {
62
- hits.set(idx, (hits.get(idx) ?? 0) + 1);
242
+ if (counts[idx] === 0) touched[touchedLen++] = idx;
243
+ counts[idx]++;
63
244
  }
64
245
  }
65
246
  const scored = [];
66
- for (const [idx, count] of hits) {
67
- const score = count / queryTrigrams.size;
247
+ const totalTrigrams = queryTrigrams.size;
248
+ for (let i = 0; i < touchedLen; i++) {
249
+ const idx = touched[i];
250
+ const score = counts[idx] / totalTrigrams;
251
+ counts[idx] = 0;
68
252
  if (score >= threshold) {
69
- scored.push({ idx, score });
253
+ scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) });
70
254
  }
71
255
  }
72
256
  scored.sort((a, b) => {
73
257
  if (b.score !== a.score) return b.score - a.score;
258
+ return b.matchRank - a.matchRank;
259
+ });
260
+ const windowSize = Math.max(limit * 4, 50);
261
+ const window = scored.slice(0, windowSize);
262
+ window.sort((a, b) => {
263
+ if (b.score !== a.score) return b.score - a.score;
264
+ if (b.matchRank !== a.matchRank) return b.matchRank - a.matchRank;
74
265
  const ra = index.records[a.idx];
75
266
  const rb = index.records[b.idx];
76
- return ra.provinceNameTh.localeCompare(rb.provinceNameTh, "th") || ra.amphureNameTh.localeCompare(rb.amphureNameTh, "th") || ra.tambonNameTh.localeCompare(rb.tambonNameTh, "th");
267
+ return TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) || TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) || TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh);
77
268
  });
78
- return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
269
+ return window.slice(0, limit).map(({ idx }) => ({ ...index.records[idx] }));
79
270
  }
80
271
 
81
272
  // src/core/formatter.ts
82
- function formatThaiAddressSuggestion(record) {
273
+ function formatThaiAddressSuggestion(record, options) {
274
+ const labelTh = `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`;
275
+ const labelEn = `${record.tambonNameEn} > ${record.amphureNameEn} > ${record.provinceNameEn} ${record.zipCode}`;
83
276
  return {
84
277
  id: String(record.tambonId),
85
- label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,
278
+ label: options?.locale === "en" ? labelEn : labelTh,
279
+ labelTh,
280
+ labelEn,
86
281
  tambon: record.tambonNameTh,
87
282
  tambonEn: record.tambonNameEn,
88
283
  amphure: record.amphureNameTh,
@@ -113,13 +308,36 @@ function resolveThaiAddress(record) {
113
308
 
114
309
  // src/react/useThaiAddressAutocomplete.ts
115
310
  function useThaiAddressAutocomplete(options) {
116
- const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
311
+ const {
312
+ index,
313
+ limit = 10,
314
+ debounce: debounceMs = 200,
315
+ threshold = 0.4,
316
+ zipLimit,
317
+ initialQuery = "",
318
+ locale,
319
+ onSelect
320
+ } = options;
117
321
  const indexRef = useRef(index);
118
322
  indexRef.current = index;
119
- const [query, setQuery] = useState("");
323
+ const [query, setQuery] = useState(initialQuery);
120
324
  const [suggestions, setSuggestions] = useState([]);
121
325
  const timerRef = useRef(null);
122
326
  const matchedRecordsMapRef = useRef(/* @__PURE__ */ new Map());
327
+ const suppressedQueryRef = useRef(initialQuery);
328
+ const runSearch = useCallback(
329
+ (q, idx) => {
330
+ const records = searchThaiAddress(idx, q, { limit, threshold, zipLimit });
331
+ const formatted = records.map((record) => formatThaiAddressSuggestion(record, { locale }));
332
+ const recordMap = /* @__PURE__ */ new Map();
333
+ for (let i = 0; i < records.length; i++) {
334
+ recordMap.set(formatted[i].id, records[i]);
335
+ }
336
+ matchedRecordsMapRef.current = recordMap;
337
+ setSuggestions(formatted);
338
+ },
339
+ [limit, threshold, zipLimit, locale]
340
+ );
123
341
  useEffect(() => {
124
342
  if (timerRef.current) clearTimeout(timerRef.current);
125
343
  if (query.length === 0) {
@@ -127,53 +345,52 @@ function useThaiAddressAutocomplete(options) {
127
345
  matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
128
346
  return;
129
347
  }
348
+ if (query === suppressedQueryRef.current) return;
349
+ suppressedQueryRef.current = "";
130
350
  timerRef.current = setTimeout(() => {
131
- const records = searchThaiAddress(indexRef.current, query, { limit, threshold });
132
- const formatted = records.map(formatThaiAddressSuggestion);
133
- const recordMap = /* @__PURE__ */ new Map();
134
- for (let i = 0; i < records.length; i++) {
135
- recordMap.set(formatted[i].id, records[i]);
136
- }
137
- matchedRecordsMapRef.current = recordMap;
138
- setSuggestions(formatted);
351
+ runSearch(query, indexRef.current);
139
352
  }, debounceMs);
140
353
  return () => {
141
354
  if (timerRef.current) clearTimeout(timerRef.current);
142
355
  };
143
- }, [query, limit, threshold, debounceMs]);
356
+ }, [query, limit, threshold, debounceMs, zipLimit, locale, runSearch]);
144
357
  useEffect(() => {
145
358
  if (query.length === 0) return;
359
+ if (query === suppressedQueryRef.current) return;
146
360
  if (timerRef.current) clearTimeout(timerRef.current);
147
- const records = searchThaiAddress(index, query, { limit, threshold });
148
- const formatted = records.map(formatThaiAddressSuggestion);
149
- const recordMap = /* @__PURE__ */ new Map();
150
- for (let i = 0; i < records.length; i++) {
151
- recordMap.set(formatted[i].id, records[i]);
152
- }
153
- matchedRecordsMapRef.current = recordMap;
154
- setSuggestions(formatted);
361
+ runSearch(query, index);
155
362
  }, [index]);
156
363
  const selectSuggestion = useCallback(
157
364
  (item) => {
158
365
  if (timerRef.current) clearTimeout(timerRef.current);
159
366
  const record = matchedRecordsMapRef.current.get(item.id);
160
367
  if (!record) {
161
- throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
368
+ return null;
162
369
  }
163
370
  setSuggestions([]);
164
371
  matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
165
- return resolveThaiAddress(record);
372
+ const resolved = resolveThaiAddress(record);
373
+ onSelect?.(resolved);
374
+ return resolved;
166
375
  },
167
- []
376
+ [onSelect]
168
377
  );
169
378
  const clear = useCallback(() => {
170
379
  setQuery("");
171
380
  setSuggestions([]);
172
381
  matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
173
382
  }, []);
383
+ const setQuerySilent = useCallback((value) => {
384
+ if (timerRef.current) clearTimeout(timerRef.current);
385
+ suppressedQueryRef.current = value;
386
+ setSuggestions([]);
387
+ matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
388
+ setQuery(value);
389
+ }, []);
174
390
  return {
175
391
  query,
176
392
  setQuery,
393
+ setQuerySilent,
177
394
  suggestions,
178
395
  isOpen: query.length > 0 && suggestions.length > 0,
179
396
  selectSuggestion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thaizip",
3
- "version": "0.6.0",
3
+ "version": "0.7.3",
4
4
  "description": "Fast, fuzzy Thai address autocomplete library (headless + React)",
5
5
  "keywords": [
6
6
  "thai",
@@ -8,13 +8,13 @@
8
8
  "autocomplete",
9
9
  "thailand"
10
10
  ],
11
- "homepage": "https://github.com/naay99999/thai-address-lib#readme",
11
+ "homepage": "https://github.com/naay99999/thai-zip#readme",
12
12
  "bugs": {
13
- "url": "https://github.com/naay99999/thai-address-lib/issues"
13
+ "url": "https://github.com/naay99999/thai-zip/issues"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/naay99999/thai-address-lib.git"
17
+ "url": "git+https://github.com/naay99999/thai-zip.git"
18
18
  },
19
19
  "license": "MIT",
20
20
  "author": "narablyat",
@@ -40,7 +40,10 @@
40
40
  "module": "./dist/index.js",
41
41
  "types": "./dist/index.d.ts",
42
42
  "files": [
43
- "dist"
43
+ "dist/*.js",
44
+ "dist/*.cjs",
45
+ "dist/*.d.ts",
46
+ "dist/*.d.cts"
44
47
  ],
45
48
  "sideEffects": false,
46
49
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { BuildIndexOptions, RawData, RawTambon, ThaiAddressRecord, TrigramIndex } from '../types'\n\nfunction addTrigrams(map: Map<string, Set<number>>, trigrams: Set<string>, idx: number): void {\n for (const trigram of trigrams) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n}\n\nfunction combinedTrigrams(nameTh: string, nameEn: string): Set<string> {\n const s = new Set<string>()\n for (const t of extractTrigrams(nameTh)) s.add(t)\n for (const t of extractTrigrams(nameEn)) s.add(t)\n return s\n}\n\nexport function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n // Pre-compute trigrams for shared province and amphure fields to avoid\n // redundant normalization when multiple tambons share the same parent.\n const provTrigrams = new Map<number, Set<string>>()\n for (const [id, prov] of provMap) {\n provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en))\n }\n const ampTrigrams = new Map<number, Set<string>>()\n for (const [id, amp] of ampMap) {\n ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en))\n }\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n // Tambon-specific fields (unique per record)\n addTrigrams(map, extractTrigrams(record.tambonNameTh), idx)\n addTrigrams(map, extractTrigrams(record.tambonNameEn), idx)\n addTrigrams(map, extractTrigrams(record.zipCode), idx)\n // Reuse pre-computed province and amphure trigrams\n addTrigrams(map, provTrigrams.get(province.id)!, idx)\n addTrigrams(map, ampTrigrams.get(amphure.id)!, idx)\n }\n\n return { map, records, zipIndex }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n"]}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { BuildIndexOptions, RawData, RawTambon, ThaiAddressRecord, TrigramIndex } from '../types'\n\nfunction addTrigrams(map: Map<string, Set<number>>, trigrams: Set<string>, idx: number): void {\n for (const trigram of trigrams) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n}\n\nfunction combinedTrigrams(nameTh: string, nameEn: string): Set<string> {\n const s = new Set<string>()\n for (const t of extractTrigrams(nameTh)) s.add(t)\n for (const t of extractTrigrams(nameEn)) s.add(t)\n return s\n}\n\nexport function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n // Pre-compute trigrams for shared province and amphure fields to avoid\n // redundant normalization when multiple tambons share the same parent.\n const provTrigrams = new Map<number, Set<string>>()\n for (const [id, prov] of provMap) {\n provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en))\n }\n const ampTrigrams = new Map<number, Set<string>>()\n for (const [id, amp] of ampMap) {\n ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en))\n }\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n // Tambon-specific fields (unique per record)\n addTrigrams(map, extractTrigrams(record.tambonNameTh), idx)\n addTrigrams(map, extractTrigrams(record.tambonNameEn), idx)\n addTrigrams(map, extractTrigrams(record.zipCode), idx)\n // Reuse pre-computed province and amphure trigrams\n addTrigrams(map, provTrigrams.get(province.id)!, idx)\n addTrigrams(map, ampTrigrams.get(amphure.id)!, idx)\n }\n\n return { map, records, zipIndex }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":["useRef","useState","useEffect","useCallback"],"mappings":"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","file":"react.cjs","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n","// NOTE (SZ-4): CJS consumers (`dist/index.cjs`) cannot tree-shake this hook out of the bundle.\n// If you are using the CJS build in a non-React environment, import only from the core entry\n// or use the ESM build with a bundler that supports tree-shaking.\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { searchThaiAddress } from '../core/search'\nimport { formatThaiAddressSuggestion } from '../core/formatter'\nimport { resolveThaiAddress } from '../core/resolver'\nimport type {\n UseThaiAddressAutocompleteOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n ResolvedThaiAddress,\n} from '../types'\n\n/**\n * Headless Thai address autocomplete hook.\n *\n * @param options.index - The TrigramIndex to search against. The hook reads `index`\n * via an internal ref, so a new object reference does NOT restart the debounce\n * timer. When `index` itself changes (e.g. swapped for a custom dataset), any\n * pending query is re-searched immediately with the new index.\n * @param options.limit - Maximum number of suggestions (default 10).\n * @param options.debounce - Debounce delay in ms (default 200).\n * @param options.threshold - Minimum trigram score 0–1 (default 0.4).\n */\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n // Keep index in a ref so a new object reference from the caller doesn't\n // re-trigger the search effect. The ref is updated synchronously each render\n // so the debounce callback always reads the latest value.\n const indexRef = useRef(index)\n indexRef.current = index\n\n const [query, setQuery] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(indexRef.current, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n // `index` is intentionally omitted — it is read via indexRef.current so that\n // a new reference from the caller does not restart the debounce timer.\n }, [query, limit, threshold, debounceMs])\n\n // When `index` changes (e.g. loaded after user started typing, or swapped for a\n // custom dataset), re-run the search immediately rather than waiting for the\n // next keystroke.\n useEffect(() => {\n if (query.length === 0) return\n if (timerRef.current) clearTimeout(timerRef.current)\n const records = searchThaiAddress(index, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [index])\n\n /**\n * Resolves a suggestion to a full ThaiAddress and clears the suggestions list.\n *\n * Note: `query` is intentionally left unchanged so the input field retains the\n * typed text. Call `clear()` afterwards if you want to reset the input as well.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n if (timerRef.current) clearTimeout(timerRef.current)\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n throw new Error(`[thaizip] No record found for suggestion id \"${item.id}\". Make sure to use suggestions returned by this hook.`)\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQuery('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n return {\n query,\n setQuery,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}
package/dist/react.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n","// NOTE (SZ-4): CJS consumers (`dist/index.cjs`) cannot tree-shake this hook out of the bundle.\n// If you are using the CJS build in a non-React environment, import only from the core entry\n// or use the ESM build with a bundler that supports tree-shaking.\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { searchThaiAddress } from '../core/search'\nimport { formatThaiAddressSuggestion } from '../core/formatter'\nimport { resolveThaiAddress } from '../core/resolver'\nimport type {\n UseThaiAddressAutocompleteOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n ResolvedThaiAddress,\n} from '../types'\n\n/**\n * Headless Thai address autocomplete hook.\n *\n * @param options.index - The TrigramIndex to search against. The hook reads `index`\n * via an internal ref, so a new object reference does NOT restart the debounce\n * timer. When `index` itself changes (e.g. swapped for a custom dataset), any\n * pending query is re-searched immediately with the new index.\n * @param options.limit - Maximum number of suggestions (default 10).\n * @param options.debounce - Debounce delay in ms (default 200).\n * @param options.threshold - Minimum trigram score 0–1 (default 0.4).\n */\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n // Keep index in a ref so a new object reference from the caller doesn't\n // re-trigger the search effect. The ref is updated synchronously each render\n // so the debounce callback always reads the latest value.\n const indexRef = useRef(index)\n indexRef.current = index\n\n const [query, setQuery] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(indexRef.current, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n // `index` is intentionally omitted — it is read via indexRef.current so that\n // a new reference from the caller does not restart the debounce timer.\n }, [query, limit, threshold, debounceMs])\n\n // When `index` changes (e.g. loaded after user started typing, or swapped for a\n // custom dataset), re-run the search immediately rather than waiting for the\n // next keystroke.\n useEffect(() => {\n if (query.length === 0) return\n if (timerRef.current) clearTimeout(timerRef.current)\n const records = searchThaiAddress(index, query, { limit, threshold })\n const formatted = records.map(formatThaiAddressSuggestion)\n const recordMap = new Map<string, ThaiAddressRecord>()\n for (let i = 0; i < records.length; i++) {\n recordMap.set(formatted[i].id, records[i])\n }\n matchedRecordsMapRef.current = recordMap\n setSuggestions(formatted)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [index])\n\n /**\n * Resolves a suggestion to a full ThaiAddress and clears the suggestions list.\n *\n * Note: `query` is intentionally left unchanged so the input field retains the\n * typed text. Call `clear()` afterwards if you want to reset the input as well.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n if (timerRef.current) clearTimeout(timerRef.current)\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n throw new Error(`[thaizip] No record found for suggestion id \"${item.id}\". Make sure to use suggestions returned by this hook.`)\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQuery('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n return {\n query,\n setQuery,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}