thaizip 0.1.0
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 +229 -0
- package/data/thai_amphures.json +8282 -0
- package/data/thai_geographies.json +26 -0
- package/data/thai_provinces.json +695 -0
- package/data/thai_tambons.json +74532 -0
- package/dist/data.cjs +103402 -0
- package/dist/data.d.cts +22 -0
- package/dist/data.d.ts +22 -0
- package/dist/data.js +103400 -0
- package/dist/index.cjs +210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +99 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +203 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/core/normalizer.ts
|
|
6
|
+
var TONE_MARKS = /[\u0E48-\u0E4B\u0E47\u0E4C]/g;
|
|
7
|
+
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/;
|
|
8
|
+
function normalizeThaiAddressText(input) {
|
|
9
|
+
if (!input) return "";
|
|
10
|
+
let text = input.trim();
|
|
11
|
+
text = text.replace(ADDRESS_PREFIXES, "");
|
|
12
|
+
text = text.replace(TONE_MARKS, "");
|
|
13
|
+
text = text.toLowerCase();
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/core/trigrams.ts
|
|
18
|
+
function extractTrigrams(text) {
|
|
19
|
+
const normalized = normalizeThaiAddressText(text);
|
|
20
|
+
const result = /* @__PURE__ */ new Set();
|
|
21
|
+
if (normalized.length === 0) return result;
|
|
22
|
+
if (normalized.length < 3) {
|
|
23
|
+
result.add(normalized);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i <= normalized.length - 3; i++) {
|
|
27
|
+
result.add(normalized.slice(i, i + 3));
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/core/indexer.ts
|
|
33
|
+
function buildThaiAddressIndex(data) {
|
|
34
|
+
const { geographies, provinces, amphures, tambons } = data;
|
|
35
|
+
const geoMap = new Map(
|
|
36
|
+
geographies.filter((g) => !g.deleted_at).map((g) => [g.id, g])
|
|
37
|
+
);
|
|
38
|
+
const provMap = new Map(
|
|
39
|
+
provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
|
|
40
|
+
);
|
|
41
|
+
const ampMap = new Map(
|
|
42
|
+
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
43
|
+
);
|
|
44
|
+
const records = [];
|
|
45
|
+
const map = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const tambon of tambons) {
|
|
47
|
+
if (tambon.deleted_at) continue;
|
|
48
|
+
const amphure = ampMap.get(tambon.amphure_id);
|
|
49
|
+
if (!amphure) continue;
|
|
50
|
+
const province = provMap.get(amphure.province_id);
|
|
51
|
+
if (!province) continue;
|
|
52
|
+
const geography = geoMap.get(province.geography_id);
|
|
53
|
+
if (!geography) continue;
|
|
54
|
+
const record = {
|
|
55
|
+
geographyId: geography.id,
|
|
56
|
+
geographyNameTh: geography.name,
|
|
57
|
+
provinceId: province.id,
|
|
58
|
+
provinceNameTh: province.name_th,
|
|
59
|
+
provinceNameEn: province.name_en,
|
|
60
|
+
amphureId: amphure.id,
|
|
61
|
+
amphureNameTh: amphure.name_th,
|
|
62
|
+
amphureNameEn: amphure.name_en,
|
|
63
|
+
tambonId: tambon.id,
|
|
64
|
+
tambonNameTh: tambon.name_th,
|
|
65
|
+
tambonNameEn: tambon.name_en,
|
|
66
|
+
zipCode: String(tambon.zip_code)
|
|
67
|
+
};
|
|
68
|
+
const idx = records.length;
|
|
69
|
+
records.push(record);
|
|
70
|
+
const fields = [
|
|
71
|
+
record.tambonNameTh,
|
|
72
|
+
record.tambonNameEn,
|
|
73
|
+
record.amphureNameTh,
|
|
74
|
+
record.amphureNameEn,
|
|
75
|
+
record.provinceNameTh,
|
|
76
|
+
record.provinceNameEn,
|
|
77
|
+
record.zipCode
|
|
78
|
+
];
|
|
79
|
+
for (const field of fields) {
|
|
80
|
+
for (const trigram of extractTrigrams(field)) {
|
|
81
|
+
let set = map.get(trigram);
|
|
82
|
+
if (!set) {
|
|
83
|
+
set = /* @__PURE__ */ new Set();
|
|
84
|
+
map.set(trigram, set);
|
|
85
|
+
}
|
|
86
|
+
set.add(idx);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { map, records };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/core/search.ts
|
|
94
|
+
var ZIP_CODE_RE = /^\d+$/;
|
|
95
|
+
function searchThaiAddress(index, query, options) {
|
|
96
|
+
const limit = options?.limit ?? 10;
|
|
97
|
+
const threshold = options?.threshold ?? 0.4;
|
|
98
|
+
if (!query) return [];
|
|
99
|
+
const normalized = normalizeThaiAddressText(query);
|
|
100
|
+
if (normalized.length === 0) return [];
|
|
101
|
+
if (ZIP_CODE_RE.test(normalized)) {
|
|
102
|
+
if (normalized.length < 2) return [];
|
|
103
|
+
return index.records.filter((r) => r.zipCode.startsWith(normalized)).slice(0, limit);
|
|
104
|
+
}
|
|
105
|
+
const queryTrigrams = extractTrigrams(normalized);
|
|
106
|
+
if (queryTrigrams.size === 0) return [];
|
|
107
|
+
const hits = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const trigram of queryTrigrams) {
|
|
109
|
+
const candidates = index.map.get(trigram);
|
|
110
|
+
if (!candidates) continue;
|
|
111
|
+
for (const idx of candidates) {
|
|
112
|
+
hits.set(idx, (hits.get(idx) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const scored = [];
|
|
116
|
+
for (const [idx, count] of hits) {
|
|
117
|
+
const score = count / queryTrigrams.size;
|
|
118
|
+
if (score >= threshold) {
|
|
119
|
+
scored.push({ idx, score });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
scored.sort((a, b) => b.score - a.score);
|
|
123
|
+
return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/core/formatter.ts
|
|
127
|
+
function formatThaiAddressSuggestion(record) {
|
|
128
|
+
return {
|
|
129
|
+
id: String(record.tambonId),
|
|
130
|
+
label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,
|
|
131
|
+
tambon: record.tambonNameTh,
|
|
132
|
+
amphure: record.amphureNameTh,
|
|
133
|
+
province: record.provinceNameTh,
|
|
134
|
+
zipCode: record.zipCode
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/core/resolver.ts
|
|
139
|
+
function resolveThaiAddress(record) {
|
|
140
|
+
return {
|
|
141
|
+
tambon: record.tambonNameTh,
|
|
142
|
+
amphure: record.amphureNameTh,
|
|
143
|
+
province: record.provinceNameTh,
|
|
144
|
+
zipCode: record.zipCode,
|
|
145
|
+
subdistrict: record.tambonNameTh,
|
|
146
|
+
district: record.amphureNameTh,
|
|
147
|
+
postalCode: record.zipCode
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function useThaiAddressAutocomplete(options) {
|
|
151
|
+
const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
|
|
152
|
+
const [query, setQueryState] = react.useState("");
|
|
153
|
+
const [suggestions, setSuggestions] = react.useState([]);
|
|
154
|
+
const timerRef = react.useRef(null);
|
|
155
|
+
const matchedRecordsRef = react.useRef([]);
|
|
156
|
+
const setQuery = react.useCallback((value) => {
|
|
157
|
+
setQueryState(value);
|
|
158
|
+
}, []);
|
|
159
|
+
react.useEffect(() => {
|
|
160
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
161
|
+
if (query.length === 0) {
|
|
162
|
+
setSuggestions([]);
|
|
163
|
+
matchedRecordsRef.current = [];
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
timerRef.current = setTimeout(() => {
|
|
167
|
+
const records = searchThaiAddress(index, query, { limit, threshold });
|
|
168
|
+
matchedRecordsRef.current = records;
|
|
169
|
+
setSuggestions(records.map(formatThaiAddressSuggestion));
|
|
170
|
+
}, debounceMs);
|
|
171
|
+
return () => {
|
|
172
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
173
|
+
};
|
|
174
|
+
}, [query, index, limit, threshold, debounceMs]);
|
|
175
|
+
const selectSuggestion = react.useCallback(
|
|
176
|
+
(item) => {
|
|
177
|
+
const idx = matchedRecordsRef.current.findIndex((r) => String(r.tambonId) === item.id);
|
|
178
|
+
const record = matchedRecordsRef.current[idx];
|
|
179
|
+
if (!record) {
|
|
180
|
+
throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
|
|
181
|
+
}
|
|
182
|
+
setSuggestions([]);
|
|
183
|
+
matchedRecordsRef.current = [];
|
|
184
|
+
return resolveThaiAddress(record);
|
|
185
|
+
},
|
|
186
|
+
[]
|
|
187
|
+
);
|
|
188
|
+
const clear = react.useCallback(() => {
|
|
189
|
+
setQueryState("");
|
|
190
|
+
setSuggestions([]);
|
|
191
|
+
matchedRecordsRef.current = [];
|
|
192
|
+
}, []);
|
|
193
|
+
return {
|
|
194
|
+
query,
|
|
195
|
+
setQuery,
|
|
196
|
+
suggestions,
|
|
197
|
+
isOpen: query.length > 0 && suggestions.length > 0,
|
|
198
|
+
selectSuggestion,
|
|
199
|
+
clear
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
exports.buildThaiAddressIndex = buildThaiAddressIndex;
|
|
204
|
+
exports.formatThaiAddressSuggestion = formatThaiAddressSuggestion;
|
|
205
|
+
exports.normalizeThaiAddressText = normalizeThaiAddressText;
|
|
206
|
+
exports.resolveThaiAddress = resolveThaiAddress;
|
|
207
|
+
exports.searchThaiAddress = searchThaiAddress;
|
|
208
|
+
exports.useThaiAddressAutocomplete = useThaiAddressAutocomplete;
|
|
209
|
+
//# sourceMappingURL=index.cjs.map
|
|
210
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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","../src/react/useThaiAddressAutocomplete.ts"],"names":["useState","useRef","useCallback","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["const TONE_MARKS = /[\\u0E48-\\u0E4B\\u0E47\\u0E4C]/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, '')\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\nexport function extractTrigrams(text: string): Set<string> {\n const normalized = normalizeThaiAddressText(text)\n const result = new Set<string>()\n if (normalized.length === 0) return result\n if (normalized.length < 3) {\n result.add(normalized)\n return result\n }\n for (let i = 0; i <= normalized.length - 3; i++) {\n result.add(normalized.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { geographies, provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const geoMap = new Map(\n geographies.filter(g => !g.deleted_at).map(g => [g.id, g])\n )\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 const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<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) continue\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const geography = geoMap.get(province.geography_id)\n if (!geography) continue\n\n const record: ThaiAddressRecord = {\n geographyId: geography.id,\n geographyNameTh: geography.name,\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 const fields = [\n record.tambonNameTh,\n record.tambonNameEn,\n record.amphureNameTh,\n record.amphureNameEn,\n record.provinceNameTh,\n record.provinceNameEn,\n record.zipCode,\n ]\n\n for (const field of fields) {\n for (const trigram of extractTrigrams(field)) {\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 }\n\n return { map, records }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigrams } 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 (!query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) 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 return index.records\n .filter(r => r.zipCode.startsWith(normalized))\n .slice(0, limit)\n }\n\n const queryTrigrams = extractTrigrams(normalized)\n if (queryTrigrams.size === 0) return []\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) => b.score - a.score)\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 amphure: record.amphureNameTh,\n province: record.provinceNameTh,\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 amphure: record.amphureNameTh,\n province: record.provinceNameTh,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n district: record.amphureNameTh,\n postalCode: record.zipCode,\n }\n}\n","import { 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\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n const [query, setQueryState] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n // Parallel array to suggestions — same index, same order\n const matchedRecordsRef = useRef<ThaiAddressRecord[]>([])\n\n const setQuery = useCallback((value: string) => {\n setQueryState(value)\n }, [])\n\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsRef.current = []\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(index, query, { limit, threshold })\n matchedRecordsRef.current = records\n setSuggestions(records.map(formatThaiAddressSuggestion))\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\n\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n // O(1) lookup via parallel array — same index as suggestions array\n const idx = matchedRecordsRef.current.findIndex(r => String(r.tambonId) === item.id)\n const record = matchedRecordsRef.current[idx]\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 matchedRecordsRef.current = []\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQueryState('')\n setSuggestions([])\n matchedRecordsRef.current = []\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/index.d.cts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
type ThaiAddressRecord = {
|
|
2
|
+
geographyId: number;
|
|
3
|
+
geographyNameTh: string;
|
|
4
|
+
provinceId: number;
|
|
5
|
+
provinceNameTh: string;
|
|
6
|
+
provinceNameEn: string;
|
|
7
|
+
amphureId: number;
|
|
8
|
+
amphureNameTh: string;
|
|
9
|
+
amphureNameEn: string;
|
|
10
|
+
tambonId: number;
|
|
11
|
+
tambonNameTh: string;
|
|
12
|
+
tambonNameEn: string;
|
|
13
|
+
zipCode: string;
|
|
14
|
+
};
|
|
15
|
+
type TrigramIndex = {
|
|
16
|
+
map: Map<string, Set<number>>;
|
|
17
|
+
records: ThaiAddressRecord[];
|
|
18
|
+
};
|
|
19
|
+
type ThaiAddressSuggestion = {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
tambon: string;
|
|
23
|
+
amphure: string;
|
|
24
|
+
province: string;
|
|
25
|
+
zipCode: string;
|
|
26
|
+
};
|
|
27
|
+
type ResolvedThaiAddress = {
|
|
28
|
+
tambon: string;
|
|
29
|
+
amphure: string;
|
|
30
|
+
province: string;
|
|
31
|
+
zipCode: string;
|
|
32
|
+
subdistrict: string;
|
|
33
|
+
district: string;
|
|
34
|
+
postalCode: string;
|
|
35
|
+
};
|
|
36
|
+
type SearchOptions = {
|
|
37
|
+
limit?: number;
|
|
38
|
+
threshold?: number;
|
|
39
|
+
};
|
|
40
|
+
type UseThaiAddressAutocompleteOptions = {
|
|
41
|
+
index: TrigramIndex;
|
|
42
|
+
limit?: number;
|
|
43
|
+
debounce?: number;
|
|
44
|
+
threshold?: number;
|
|
45
|
+
};
|
|
46
|
+
type RawGeography = {
|
|
47
|
+
id: number;
|
|
48
|
+
name: string;
|
|
49
|
+
deleted_at: string | null;
|
|
50
|
+
};
|
|
51
|
+
type RawProvince = {
|
|
52
|
+
id: number;
|
|
53
|
+
name_th: string;
|
|
54
|
+
name_en: string;
|
|
55
|
+
geography_id: number;
|
|
56
|
+
deleted_at: string | null;
|
|
57
|
+
};
|
|
58
|
+
type RawAmphure = {
|
|
59
|
+
id: number;
|
|
60
|
+
name_th: string;
|
|
61
|
+
name_en: string;
|
|
62
|
+
province_id: number;
|
|
63
|
+
deleted_at: string | null;
|
|
64
|
+
};
|
|
65
|
+
type RawTambon = {
|
|
66
|
+
id: number;
|
|
67
|
+
zip_code: number;
|
|
68
|
+
name_th: string;
|
|
69
|
+
name_en: string;
|
|
70
|
+
amphure_id: number;
|
|
71
|
+
deleted_at: string | null;
|
|
72
|
+
};
|
|
73
|
+
type RawData = {
|
|
74
|
+
geographies: RawGeography[];
|
|
75
|
+
provinces: RawProvince[];
|
|
76
|
+
amphures: RawAmphure[];
|
|
77
|
+
tambons: RawTambon[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
declare function buildThaiAddressIndex(data: RawData): TrigramIndex;
|
|
81
|
+
|
|
82
|
+
declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
|
|
83
|
+
|
|
84
|
+
declare function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion;
|
|
85
|
+
|
|
86
|
+
declare function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress;
|
|
87
|
+
|
|
88
|
+
declare function normalizeThaiAddressText(input: string): string;
|
|
89
|
+
|
|
90
|
+
declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions): {
|
|
91
|
+
query: string;
|
|
92
|
+
setQuery: (value: string) => void;
|
|
93
|
+
suggestions: ThaiAddressSuggestion[];
|
|
94
|
+
isOpen: boolean;
|
|
95
|
+
selectSuggestion: (item: ThaiAddressSuggestion) => ResolvedThaiAddress;
|
|
96
|
+
clear: () => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
type ThaiAddressRecord = {
|
|
2
|
+
geographyId: number;
|
|
3
|
+
geographyNameTh: string;
|
|
4
|
+
provinceId: number;
|
|
5
|
+
provinceNameTh: string;
|
|
6
|
+
provinceNameEn: string;
|
|
7
|
+
amphureId: number;
|
|
8
|
+
amphureNameTh: string;
|
|
9
|
+
amphureNameEn: string;
|
|
10
|
+
tambonId: number;
|
|
11
|
+
tambonNameTh: string;
|
|
12
|
+
tambonNameEn: string;
|
|
13
|
+
zipCode: string;
|
|
14
|
+
};
|
|
15
|
+
type TrigramIndex = {
|
|
16
|
+
map: Map<string, Set<number>>;
|
|
17
|
+
records: ThaiAddressRecord[];
|
|
18
|
+
};
|
|
19
|
+
type ThaiAddressSuggestion = {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
tambon: string;
|
|
23
|
+
amphure: string;
|
|
24
|
+
province: string;
|
|
25
|
+
zipCode: string;
|
|
26
|
+
};
|
|
27
|
+
type ResolvedThaiAddress = {
|
|
28
|
+
tambon: string;
|
|
29
|
+
amphure: string;
|
|
30
|
+
province: string;
|
|
31
|
+
zipCode: string;
|
|
32
|
+
subdistrict: string;
|
|
33
|
+
district: string;
|
|
34
|
+
postalCode: string;
|
|
35
|
+
};
|
|
36
|
+
type SearchOptions = {
|
|
37
|
+
limit?: number;
|
|
38
|
+
threshold?: number;
|
|
39
|
+
};
|
|
40
|
+
type UseThaiAddressAutocompleteOptions = {
|
|
41
|
+
index: TrigramIndex;
|
|
42
|
+
limit?: number;
|
|
43
|
+
debounce?: number;
|
|
44
|
+
threshold?: number;
|
|
45
|
+
};
|
|
46
|
+
type RawGeography = {
|
|
47
|
+
id: number;
|
|
48
|
+
name: string;
|
|
49
|
+
deleted_at: string | null;
|
|
50
|
+
};
|
|
51
|
+
type RawProvince = {
|
|
52
|
+
id: number;
|
|
53
|
+
name_th: string;
|
|
54
|
+
name_en: string;
|
|
55
|
+
geography_id: number;
|
|
56
|
+
deleted_at: string | null;
|
|
57
|
+
};
|
|
58
|
+
type RawAmphure = {
|
|
59
|
+
id: number;
|
|
60
|
+
name_th: string;
|
|
61
|
+
name_en: string;
|
|
62
|
+
province_id: number;
|
|
63
|
+
deleted_at: string | null;
|
|
64
|
+
};
|
|
65
|
+
type RawTambon = {
|
|
66
|
+
id: number;
|
|
67
|
+
zip_code: number;
|
|
68
|
+
name_th: string;
|
|
69
|
+
name_en: string;
|
|
70
|
+
amphure_id: number;
|
|
71
|
+
deleted_at: string | null;
|
|
72
|
+
};
|
|
73
|
+
type RawData = {
|
|
74
|
+
geographies: RawGeography[];
|
|
75
|
+
provinces: RawProvince[];
|
|
76
|
+
amphures: RawAmphure[];
|
|
77
|
+
tambons: RawTambon[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
declare function buildThaiAddressIndex(data: RawData): TrigramIndex;
|
|
81
|
+
|
|
82
|
+
declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
|
|
83
|
+
|
|
84
|
+
declare function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion;
|
|
85
|
+
|
|
86
|
+
declare function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress;
|
|
87
|
+
|
|
88
|
+
declare function normalizeThaiAddressText(input: string): string;
|
|
89
|
+
|
|
90
|
+
declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions): {
|
|
91
|
+
query: string;
|
|
92
|
+
setQuery: (value: string) => void;
|
|
93
|
+
suggestions: ThaiAddressSuggestion[];
|
|
94
|
+
isOpen: boolean;
|
|
95
|
+
selectSuggestion: (item: ThaiAddressSuggestion) => ResolvedThaiAddress;
|
|
96
|
+
clear: () => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/core/normalizer.ts
|
|
4
|
+
var TONE_MARKS = /[\u0E48-\u0E4B\u0E47\u0E4C]/g;
|
|
5
|
+
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/;
|
|
6
|
+
function normalizeThaiAddressText(input) {
|
|
7
|
+
if (!input) return "";
|
|
8
|
+
let text = input.trim();
|
|
9
|
+
text = text.replace(ADDRESS_PREFIXES, "");
|
|
10
|
+
text = text.replace(TONE_MARKS, "");
|
|
11
|
+
text = text.toLowerCase();
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/core/trigrams.ts
|
|
16
|
+
function extractTrigrams(text) {
|
|
17
|
+
const normalized = normalizeThaiAddressText(text);
|
|
18
|
+
const result = /* @__PURE__ */ new Set();
|
|
19
|
+
if (normalized.length === 0) return result;
|
|
20
|
+
if (normalized.length < 3) {
|
|
21
|
+
result.add(normalized);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i <= normalized.length - 3; i++) {
|
|
25
|
+
result.add(normalized.slice(i, i + 3));
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/core/indexer.ts
|
|
31
|
+
function buildThaiAddressIndex(data) {
|
|
32
|
+
const { geographies, provinces, amphures, tambons } = data;
|
|
33
|
+
const geoMap = new Map(
|
|
34
|
+
geographies.filter((g) => !g.deleted_at).map((g) => [g.id, g])
|
|
35
|
+
);
|
|
36
|
+
const provMap = new Map(
|
|
37
|
+
provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
|
|
38
|
+
);
|
|
39
|
+
const ampMap = new Map(
|
|
40
|
+
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
41
|
+
);
|
|
42
|
+
const records = [];
|
|
43
|
+
const map = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const tambon of tambons) {
|
|
45
|
+
if (tambon.deleted_at) continue;
|
|
46
|
+
const amphure = ampMap.get(tambon.amphure_id);
|
|
47
|
+
if (!amphure) continue;
|
|
48
|
+
const province = provMap.get(amphure.province_id);
|
|
49
|
+
if (!province) continue;
|
|
50
|
+
const geography = geoMap.get(province.geography_id);
|
|
51
|
+
if (!geography) continue;
|
|
52
|
+
const record = {
|
|
53
|
+
geographyId: geography.id,
|
|
54
|
+
geographyNameTh: geography.name,
|
|
55
|
+
provinceId: province.id,
|
|
56
|
+
provinceNameTh: province.name_th,
|
|
57
|
+
provinceNameEn: province.name_en,
|
|
58
|
+
amphureId: amphure.id,
|
|
59
|
+
amphureNameTh: amphure.name_th,
|
|
60
|
+
amphureNameEn: amphure.name_en,
|
|
61
|
+
tambonId: tambon.id,
|
|
62
|
+
tambonNameTh: tambon.name_th,
|
|
63
|
+
tambonNameEn: tambon.name_en,
|
|
64
|
+
zipCode: String(tambon.zip_code)
|
|
65
|
+
};
|
|
66
|
+
const idx = records.length;
|
|
67
|
+
records.push(record);
|
|
68
|
+
const fields = [
|
|
69
|
+
record.tambonNameTh,
|
|
70
|
+
record.tambonNameEn,
|
|
71
|
+
record.amphureNameTh,
|
|
72
|
+
record.amphureNameEn,
|
|
73
|
+
record.provinceNameTh,
|
|
74
|
+
record.provinceNameEn,
|
|
75
|
+
record.zipCode
|
|
76
|
+
];
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
for (const trigram of extractTrigrams(field)) {
|
|
79
|
+
let set = map.get(trigram);
|
|
80
|
+
if (!set) {
|
|
81
|
+
set = /* @__PURE__ */ new Set();
|
|
82
|
+
map.set(trigram, set);
|
|
83
|
+
}
|
|
84
|
+
set.add(idx);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { map, records };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/core/search.ts
|
|
92
|
+
var ZIP_CODE_RE = /^\d+$/;
|
|
93
|
+
function searchThaiAddress(index, query, options) {
|
|
94
|
+
const limit = options?.limit ?? 10;
|
|
95
|
+
const threshold = options?.threshold ?? 0.4;
|
|
96
|
+
if (!query) return [];
|
|
97
|
+
const normalized = normalizeThaiAddressText(query);
|
|
98
|
+
if (normalized.length === 0) return [];
|
|
99
|
+
if (ZIP_CODE_RE.test(normalized)) {
|
|
100
|
+
if (normalized.length < 2) return [];
|
|
101
|
+
return index.records.filter((r) => r.zipCode.startsWith(normalized)).slice(0, limit);
|
|
102
|
+
}
|
|
103
|
+
const queryTrigrams = extractTrigrams(normalized);
|
|
104
|
+
if (queryTrigrams.size === 0) return [];
|
|
105
|
+
const hits = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const trigram of queryTrigrams) {
|
|
107
|
+
const candidates = index.map.get(trigram);
|
|
108
|
+
if (!candidates) continue;
|
|
109
|
+
for (const idx of candidates) {
|
|
110
|
+
hits.set(idx, (hits.get(idx) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const scored = [];
|
|
114
|
+
for (const [idx, count] of hits) {
|
|
115
|
+
const score = count / queryTrigrams.size;
|
|
116
|
+
if (score >= threshold) {
|
|
117
|
+
scored.push({ idx, score });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
scored.sort((a, b) => b.score - a.score);
|
|
121
|
+
return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/core/formatter.ts
|
|
125
|
+
function formatThaiAddressSuggestion(record) {
|
|
126
|
+
return {
|
|
127
|
+
id: String(record.tambonId),
|
|
128
|
+
label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,
|
|
129
|
+
tambon: record.tambonNameTh,
|
|
130
|
+
amphure: record.amphureNameTh,
|
|
131
|
+
province: record.provinceNameTh,
|
|
132
|
+
zipCode: record.zipCode
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/core/resolver.ts
|
|
137
|
+
function resolveThaiAddress(record) {
|
|
138
|
+
return {
|
|
139
|
+
tambon: record.tambonNameTh,
|
|
140
|
+
amphure: record.amphureNameTh,
|
|
141
|
+
province: record.provinceNameTh,
|
|
142
|
+
zipCode: record.zipCode,
|
|
143
|
+
subdistrict: record.tambonNameTh,
|
|
144
|
+
district: record.amphureNameTh,
|
|
145
|
+
postalCode: record.zipCode
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function useThaiAddressAutocomplete(options) {
|
|
149
|
+
const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
|
|
150
|
+
const [query, setQueryState] = useState("");
|
|
151
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
152
|
+
const timerRef = useRef(null);
|
|
153
|
+
const matchedRecordsRef = useRef([]);
|
|
154
|
+
const setQuery = useCallback((value) => {
|
|
155
|
+
setQueryState(value);
|
|
156
|
+
}, []);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
159
|
+
if (query.length === 0) {
|
|
160
|
+
setSuggestions([]);
|
|
161
|
+
matchedRecordsRef.current = [];
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
timerRef.current = setTimeout(() => {
|
|
165
|
+
const records = searchThaiAddress(index, query, { limit, threshold });
|
|
166
|
+
matchedRecordsRef.current = records;
|
|
167
|
+
setSuggestions(records.map(formatThaiAddressSuggestion));
|
|
168
|
+
}, debounceMs);
|
|
169
|
+
return () => {
|
|
170
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
171
|
+
};
|
|
172
|
+
}, [query, index, limit, threshold, debounceMs]);
|
|
173
|
+
const selectSuggestion = useCallback(
|
|
174
|
+
(item) => {
|
|
175
|
+
const idx = matchedRecordsRef.current.findIndex((r) => String(r.tambonId) === item.id);
|
|
176
|
+
const record = matchedRecordsRef.current[idx];
|
|
177
|
+
if (!record) {
|
|
178
|
+
throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
|
|
179
|
+
}
|
|
180
|
+
setSuggestions([]);
|
|
181
|
+
matchedRecordsRef.current = [];
|
|
182
|
+
return resolveThaiAddress(record);
|
|
183
|
+
},
|
|
184
|
+
[]
|
|
185
|
+
);
|
|
186
|
+
const clear = useCallback(() => {
|
|
187
|
+
setQueryState("");
|
|
188
|
+
setSuggestions([]);
|
|
189
|
+
matchedRecordsRef.current = [];
|
|
190
|
+
}, []);
|
|
191
|
+
return {
|
|
192
|
+
query,
|
|
193
|
+
setQuery,
|
|
194
|
+
suggestions,
|
|
195
|
+
isOpen: query.length > 0 && suggestions.length > 0,
|
|
196
|
+
selectSuggestion,
|
|
197
|
+
clear
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
|
|
202
|
+
//# sourceMappingURL=index.js.map
|
|
203
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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","../src/react/useThaiAddressAutocomplete.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["const TONE_MARKS = /[\\u0E48-\\u0E4B\\u0E47\\u0E4C]/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, '')\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\nexport function extractTrigrams(text: string): Set<string> {\n const normalized = normalizeThaiAddressText(text)\n const result = new Set<string>()\n if (normalized.length === 0) return result\n if (normalized.length < 3) {\n result.add(normalized)\n return result\n }\n for (let i = 0; i <= normalized.length - 3; i++) {\n result.add(normalized.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { geographies, provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const geoMap = new Map(\n geographies.filter(g => !g.deleted_at).map(g => [g.id, g])\n )\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 const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<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) continue\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const geography = geoMap.get(province.geography_id)\n if (!geography) continue\n\n const record: ThaiAddressRecord = {\n geographyId: geography.id,\n geographyNameTh: geography.name,\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 const fields = [\n record.tambonNameTh,\n record.tambonNameEn,\n record.amphureNameTh,\n record.amphureNameEn,\n record.provinceNameTh,\n record.provinceNameEn,\n record.zipCode,\n ]\n\n for (const field of fields) {\n for (const trigram of extractTrigrams(field)) {\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 }\n\n return { map, records }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigrams } 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 (!query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) 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 return index.records\n .filter(r => r.zipCode.startsWith(normalized))\n .slice(0, limit)\n }\n\n const queryTrigrams = extractTrigrams(normalized)\n if (queryTrigrams.size === 0) return []\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) => b.score - a.score)\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 amphure: record.amphureNameTh,\n province: record.provinceNameTh,\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 amphure: record.amphureNameTh,\n province: record.provinceNameTh,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n district: record.amphureNameTh,\n postalCode: record.zipCode,\n }\n}\n","import { 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\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options\n\n const [query, setQueryState] = useState('')\n const [suggestions, setSuggestions] = useState<ThaiAddressSuggestion[]>([])\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n // Parallel array to suggestions — same index, same order\n const matchedRecordsRef = useRef<ThaiAddressRecord[]>([])\n\n const setQuery = useCallback((value: string) => {\n setQueryState(value)\n }, [])\n\n useEffect(() => {\n if (timerRef.current) clearTimeout(timerRef.current)\n\n if (query.length === 0) {\n setSuggestions([])\n matchedRecordsRef.current = []\n return\n }\n\n timerRef.current = setTimeout(() => {\n const records = searchThaiAddress(index, query, { limit, threshold })\n matchedRecordsRef.current = records\n setSuggestions(records.map(formatThaiAddressSuggestion))\n }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\n\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress => {\n // O(1) lookup via parallel array — same index as suggestions array\n const idx = matchedRecordsRef.current.findIndex(r => String(r.tambonId) === item.id)\n const record = matchedRecordsRef.current[idx]\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 matchedRecordsRef.current = []\n return resolveThaiAddress(record)\n },\n [],\n )\n\n const clear = useCallback(() => {\n setQueryState('')\n setSuggestions([])\n matchedRecordsRef.current = []\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"]}
|