thaizip 0.4.0 → 0.6.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 +58 -6
- package/dist/data.cjs +62 -33
- package/dist/data.d.cts +2 -1
- package/dist/data.d.ts +2 -1
- package/dist/data.js +62 -34
- package/dist/index.cjs +40 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -18
- package/dist/index.d.ts +6 -18
- package/dist/index.js +41 -88
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +188 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +72 -0
- package/dist/react.d.ts +72 -0
- package/dist/react.js +186 -0
- package/dist/react.js.map +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1 @@
|
|
|
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.d.cts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
type ThaiAddressRecord = {
|
|
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
|
+
zipIndex: Map<string, number[]>;
|
|
19
|
+
};
|
|
20
|
+
type ThaiAddressSuggestion = {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
tambon: string;
|
|
24
|
+
tambonEn: string;
|
|
25
|
+
amphure: string;
|
|
26
|
+
amphureEn: string;
|
|
27
|
+
province: string;
|
|
28
|
+
provinceEn: string;
|
|
29
|
+
zipCode: string;
|
|
30
|
+
};
|
|
31
|
+
type ResolvedThaiAddress = {
|
|
32
|
+
tambon: string;
|
|
33
|
+
tambonEn: string;
|
|
34
|
+
amphure: string;
|
|
35
|
+
amphureEn: string;
|
|
36
|
+
province: string;
|
|
37
|
+
provinceEn: string;
|
|
38
|
+
zipCode: string;
|
|
39
|
+
subdistrict: string;
|
|
40
|
+
subdistrictEn: string;
|
|
41
|
+
district: string;
|
|
42
|
+
districtEn: string;
|
|
43
|
+
postalCode: string;
|
|
44
|
+
};
|
|
45
|
+
type UseThaiAddressAutocompleteOptions = {
|
|
46
|
+
index: TrigramIndex;
|
|
47
|
+
limit?: number;
|
|
48
|
+
debounce?: number;
|
|
49
|
+
threshold?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Headless Thai address autocomplete hook.
|
|
54
|
+
*
|
|
55
|
+
* @param options.index - The TrigramIndex to search against. The hook reads `index`
|
|
56
|
+
* via an internal ref, so a new object reference does NOT restart the debounce
|
|
57
|
+
* timer. When `index` itself changes (e.g. swapped for a custom dataset), any
|
|
58
|
+
* pending query is re-searched immediately with the new index.
|
|
59
|
+
* @param options.limit - Maximum number of suggestions (default 10).
|
|
60
|
+
* @param options.debounce - Debounce delay in ms (default 200).
|
|
61
|
+
* @param options.threshold - Minimum trigram score 0–1 (default 0.4).
|
|
62
|
+
*/
|
|
63
|
+
declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions): {
|
|
64
|
+
query: string;
|
|
65
|
+
setQuery: react.Dispatch<react.SetStateAction<string>>;
|
|
66
|
+
suggestions: ThaiAddressSuggestion[];
|
|
67
|
+
isOpen: boolean;
|
|
68
|
+
selectSuggestion: (item: ThaiAddressSuggestion) => ResolvedThaiAddress;
|
|
69
|
+
clear: () => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export { type ResolvedThaiAddress, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, useThaiAddressAutocomplete };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
type ThaiAddressRecord = {
|
|
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
|
+
zipIndex: Map<string, number[]>;
|
|
19
|
+
};
|
|
20
|
+
type ThaiAddressSuggestion = {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
tambon: string;
|
|
24
|
+
tambonEn: string;
|
|
25
|
+
amphure: string;
|
|
26
|
+
amphureEn: string;
|
|
27
|
+
province: string;
|
|
28
|
+
provinceEn: string;
|
|
29
|
+
zipCode: string;
|
|
30
|
+
};
|
|
31
|
+
type ResolvedThaiAddress = {
|
|
32
|
+
tambon: string;
|
|
33
|
+
tambonEn: string;
|
|
34
|
+
amphure: string;
|
|
35
|
+
amphureEn: string;
|
|
36
|
+
province: string;
|
|
37
|
+
provinceEn: string;
|
|
38
|
+
zipCode: string;
|
|
39
|
+
subdistrict: string;
|
|
40
|
+
subdistrictEn: string;
|
|
41
|
+
district: string;
|
|
42
|
+
districtEn: string;
|
|
43
|
+
postalCode: string;
|
|
44
|
+
};
|
|
45
|
+
type UseThaiAddressAutocompleteOptions = {
|
|
46
|
+
index: TrigramIndex;
|
|
47
|
+
limit?: number;
|
|
48
|
+
debounce?: number;
|
|
49
|
+
threshold?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Headless Thai address autocomplete hook.
|
|
54
|
+
*
|
|
55
|
+
* @param options.index - The TrigramIndex to search against. The hook reads `index`
|
|
56
|
+
* via an internal ref, so a new object reference does NOT restart the debounce
|
|
57
|
+
* timer. When `index` itself changes (e.g. swapped for a custom dataset), any
|
|
58
|
+
* pending query is re-searched immediately with the new index.
|
|
59
|
+
* @param options.limit - Maximum number of suggestions (default 10).
|
|
60
|
+
* @param options.debounce - Debounce delay in ms (default 200).
|
|
61
|
+
* @param options.threshold - Minimum trigram score 0–1 (default 0.4).
|
|
62
|
+
*/
|
|
63
|
+
declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions): {
|
|
64
|
+
query: string;
|
|
65
|
+
setQuery: react.Dispatch<react.SetStateAction<string>>;
|
|
66
|
+
suggestions: ThaiAddressSuggestion[];
|
|
67
|
+
isOpen: boolean;
|
|
68
|
+
selectSuggestion: (item: ThaiAddressSuggestion) => ResolvedThaiAddress;
|
|
69
|
+
clear: () => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export { type ResolvedThaiAddress, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, useThaiAddressAutocomplete };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/react/useThaiAddressAutocomplete.ts
|
|
4
|
+
|
|
5
|
+
// src/core/normalizer.ts
|
|
6
|
+
var TONE_MARKS = /[่-๋็์]/g;
|
|
7
|
+
var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
|
|
8
|
+
function normalizeThaiAddressText(input) {
|
|
9
|
+
if (!input) return "";
|
|
10
|
+
let text = input.trim();
|
|
11
|
+
text = text.replace(ADDRESS_PREFIXES, "").trim();
|
|
12
|
+
text = text.replace(TONE_MARKS, "");
|
|
13
|
+
text = text.toLowerCase();
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/core/trigrams.ts
|
|
18
|
+
function extractTrigramsNormalized(text) {
|
|
19
|
+
const result = /* @__PURE__ */ new Set();
|
|
20
|
+
if (text.length === 0) return result;
|
|
21
|
+
if (text.length < 3) {
|
|
22
|
+
result.add(text);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i <= text.length - 3; i++) {
|
|
26
|
+
result.add(text.slice(i, i + 3));
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/core/search.ts
|
|
32
|
+
var ZIP_CODE_RE = /^\d+$/;
|
|
33
|
+
function searchThaiAddress(index, query, options) {
|
|
34
|
+
const limit = options?.limit ?? 10;
|
|
35
|
+
const threshold = options?.threshold ?? 0.4;
|
|
36
|
+
if (!index || !query) return [];
|
|
37
|
+
const normalized = normalizeThaiAddressText(query);
|
|
38
|
+
if (normalized.length === 0) return [];
|
|
39
|
+
if (normalized.length > 300) return [];
|
|
40
|
+
if (ZIP_CODE_RE.test(normalized)) {
|
|
41
|
+
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 });
|
|
52
|
+
});
|
|
53
|
+
return matches.slice(0, limit);
|
|
54
|
+
}
|
|
55
|
+
if (normalized.length < 3 && query.trim().length < 3) return [];
|
|
56
|
+
const queryTrigrams = extractTrigramsNormalized(normalized);
|
|
57
|
+
const hits = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const trigram of queryTrigrams) {
|
|
59
|
+
const candidates = index.map.get(trigram);
|
|
60
|
+
if (!candidates) continue;
|
|
61
|
+
for (const idx of candidates) {
|
|
62
|
+
hits.set(idx, (hits.get(idx) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const scored = [];
|
|
66
|
+
for (const [idx, count] of hits) {
|
|
67
|
+
const score = count / queryTrigrams.size;
|
|
68
|
+
if (score >= threshold) {
|
|
69
|
+
scored.push({ idx, score });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
scored.sort((a, b) => {
|
|
73
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
74
|
+
const ra = index.records[a.idx];
|
|
75
|
+
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");
|
|
77
|
+
});
|
|
78
|
+
return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/core/formatter.ts
|
|
82
|
+
function formatThaiAddressSuggestion(record) {
|
|
83
|
+
return {
|
|
84
|
+
id: String(record.tambonId),
|
|
85
|
+
label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,
|
|
86
|
+
tambon: record.tambonNameTh,
|
|
87
|
+
tambonEn: record.tambonNameEn,
|
|
88
|
+
amphure: record.amphureNameTh,
|
|
89
|
+
amphureEn: record.amphureNameEn,
|
|
90
|
+
province: record.provinceNameTh,
|
|
91
|
+
provinceEn: record.provinceNameEn,
|
|
92
|
+
zipCode: record.zipCode
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/resolver.ts
|
|
97
|
+
function resolveThaiAddress(record) {
|
|
98
|
+
return {
|
|
99
|
+
tambon: record.tambonNameTh,
|
|
100
|
+
tambonEn: record.tambonNameEn,
|
|
101
|
+
amphure: record.amphureNameTh,
|
|
102
|
+
amphureEn: record.amphureNameEn,
|
|
103
|
+
province: record.provinceNameTh,
|
|
104
|
+
provinceEn: record.provinceNameEn,
|
|
105
|
+
zipCode: record.zipCode,
|
|
106
|
+
subdistrict: record.tambonNameTh,
|
|
107
|
+
subdistrictEn: record.tambonNameEn,
|
|
108
|
+
district: record.amphureNameTh,
|
|
109
|
+
districtEn: record.amphureNameEn,
|
|
110
|
+
postalCode: record.zipCode
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/react/useThaiAddressAutocomplete.ts
|
|
115
|
+
function useThaiAddressAutocomplete(options) {
|
|
116
|
+
const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
|
|
117
|
+
const indexRef = useRef(index);
|
|
118
|
+
indexRef.current = index;
|
|
119
|
+
const [query, setQuery] = useState("");
|
|
120
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
121
|
+
const timerRef = useRef(null);
|
|
122
|
+
const matchedRecordsMapRef = useRef(/* @__PURE__ */ new Map());
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
125
|
+
if (query.length === 0) {
|
|
126
|
+
setSuggestions([]);
|
|
127
|
+
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
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);
|
|
139
|
+
}, debounceMs);
|
|
140
|
+
return () => {
|
|
141
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
142
|
+
};
|
|
143
|
+
}, [query, limit, threshold, debounceMs]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (query.length === 0) return;
|
|
146
|
+
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);
|
|
155
|
+
}, [index]);
|
|
156
|
+
const selectSuggestion = useCallback(
|
|
157
|
+
(item) => {
|
|
158
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
159
|
+
const record = matchedRecordsMapRef.current.get(item.id);
|
|
160
|
+
if (!record) {
|
|
161
|
+
throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
|
|
162
|
+
}
|
|
163
|
+
setSuggestions([]);
|
|
164
|
+
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
165
|
+
return resolveThaiAddress(record);
|
|
166
|
+
},
|
|
167
|
+
[]
|
|
168
|
+
);
|
|
169
|
+
const clear = useCallback(() => {
|
|
170
|
+
setQuery("");
|
|
171
|
+
setSuggestions([]);
|
|
172
|
+
matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
|
|
173
|
+
}, []);
|
|
174
|
+
return {
|
|
175
|
+
query,
|
|
176
|
+
setQuery,
|
|
177
|
+
suggestions,
|
|
178
|
+
isOpen: query.length > 0 && suggestions.length > 0,
|
|
179
|
+
selectSuggestion,
|
|
180
|
+
clear
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { useThaiAddressAutocomplete };
|
|
185
|
+
//# sourceMappingURL=react.js.map
|
|
186
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thaizip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Fast, fuzzy Thai address autocomplete library (headless + React)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"thai",
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
"import": "./dist/index.js",
|
|
26
26
|
"require": "./dist/index.cjs"
|
|
27
27
|
},
|
|
28
|
+
"./react": {
|
|
29
|
+
"types": "./dist/react.d.ts",
|
|
30
|
+
"import": "./dist/react.js",
|
|
31
|
+
"require": "./dist/react.cjs"
|
|
32
|
+
},
|
|
28
33
|
"./data": {
|
|
29
34
|
"types": "./dist/data.d.ts",
|
|
30
35
|
"import": "./dist/data.js",
|
|
@@ -68,6 +73,6 @@
|
|
|
68
73
|
}
|
|
69
74
|
},
|
|
70
75
|
"engines": {
|
|
71
|
-
"node": ">=
|
|
76
|
+
"node": ">=18"
|
|
72
77
|
}
|
|
73
78
|
}
|