thaizip 0.7.2 → 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/LICENSE +21 -0
- package/README.md +5 -1
- package/dist/data.cjs +46 -6
- package/dist/data.d.cts +9 -0
- package/dist/data.d.ts +9 -0
- package/dist/data.js +46 -6
- package/dist/index.cjs +96 -17
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +96 -17
- package/dist/react.cjs +62 -31
- package/dist/react.d.cts +9 -0
- package/dist/react.d.ts +9 -0
- package/dist/react.js +62 -31
- package/package.json +5 -2
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
package/dist/react.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/romanize.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts","../src/react/useThaiAddressAutocomplete.ts"],"names":["useRef","useState","useCallback","useEffect"],"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","/**\n * Romanization alias expansion for Latin-script queries.\n *\n * The bundled dataset indexes only the official RTGS transliteration of each\n * name (`name_en`), so common but non-RTGS spellings a user actually types\n * (\"lardprao\", \"krungthep\", \"sukumvit\") miss the index entirely. This module\n * rewrites such a query into its RTGS form before trigram extraction.\n *\n * Contract:\n * - Input is an ALREADY-NORMALIZED query (lowercased, prefixes and tone marks\n * stripped) as produced by `normalizeThaiAddressText`.\n * - Output is a normalized query too — safe to hand straight to\n * `extractTrigramsNormalized`.\n * - Thai-script input must be returned unchanged.\n * - Must be a pure function; it runs on every keystroke.\n *\n * Design (two layers, cheapest/most-reliable first):\n *\n * 1. A curated dictionary of non-RTGS spelling -> exact RTGS string, as it\n * literally appears in a `name_en` field of the bundled dataset. Targets\n * were read off the real generated index (see the romanize benchmark\n * harness used to build this list), not guessed — a couple of entries\n * intentionally map onto data typos already baked into the dataset\n * (`loburi`, `buogkan`) because that is what is actually indexed.\n *\n * 2. A tiny, conservative cleanup pass used only when the dictionary misses:\n * collapse runs of whitespace, and strip a few English administrative\n * words (`district`, `province`, ...) that never occur inside a real\n * `name_en` value. Both were verified against the full dataset before\n * being added — see the \"rejected rules\" note below for transformations\n * that were tried and thrown out because they collide with real names.\n *\n * Rejected systematic rules (measured against the real dataset, not\n * hypothetical): a blanket `klong` -> `khlong` rewrite would break the real\n * tambons \"Mae Klong\" and \"Ho Klong\"; `muang` -> `mueang` would break ~122\n * real tambons that legitimately contain \"Muang\" as a name component (e.g.\n * \"Bang Muang\", \"Khok Muang\"); `huay` -> `huai` would break the real tambon\n * \"Huay Lueg\"; a blanket hyphen -> space rewrite would break the ~70 real\n * names that use a hyphen on purpose (e.g. \"Bang Pa-in\", the Ayutthaya\n * palace town). None of the classic AUA-vs-RTGS letter swaps (`oo` -> `u`,\n * trailing `-rd` -> `-t`, `dt` -> `t`, `bp` -> `p`, doubled consonants) had\n * any real occurrence in the traffic-prioritized query set used to validate\n * this module, so none were added — see the harness report for the\n * before/after numbers this file was tuned against.\n */\n\n// --- Layer 1: curated alias dictionary -------------------------------------\n//\n// Keys are already-normalized (lowercase) non-RTGS spellings. Values are the\n// exact RTGS substring as it appears in the dataset's `name_en` fields\n// (verified against the real generated index, not guessed).\nconst ALIASES: ReadonlyMap<string, string> = new Map([\n // --- Bangkok khet (districts) — concatenated (\"no space\") forms ---------\n ['phranakhon', 'phra nakhon'],\n ['nongchok', 'nong chok'],\n ['bangrak', 'bang rak'],\n ['bangkhen', 'bang khen'],\n ['bangkapi', 'bang kapi'],\n ['pathumwan', 'pathum wan'],\n ['patumwan', 'pathum wan'],\n ['pomprap', 'pom prap sattru phai'],\n ['phrakhanong', 'phra khanong'],\n ['minburi', 'min buri'],\n ['latkrabang', 'lat krabang'],\n ['ladkrabang', 'lat krabang'],\n ['yannawa', 'yan nawa'],\n ['phayathai', 'phaya thai'],\n ['thonburi', 'thon buri'],\n ['bangkokyai', 'bangkok yai'],\n ['huaikhwang', 'huai khwang'],\n ['huaykhwang', 'huai khwang'],\n ['huaykwang', 'huai khwang'],\n ['khlongsan', 'khlong san'],\n ['klongsan', 'khlong san'],\n ['talingchan', 'taling chan'],\n ['bangkoknoi', 'bangkok noi'],\n ['bangkhunthian', 'bang khun thian'],\n ['phasicharoen', 'phasi charoen'],\n ['nongkhaem', 'nong khaem'],\n ['ratburana', 'rat burana'],\n ['bangphlat', 'bang phlat'],\n ['dindaeng', 'din daeng'],\n ['buengkum', 'bueng kum'],\n ['bangsue', 'bang sue'],\n ['bangkholaem', 'bang kho laem'],\n ['khlongtoei', 'khlong toei'],\n ['klongtoei', 'khlong toei'],\n ['klongtoey', 'khlong toei'],\n ['khlongtoey', 'khlong toei'],\n ['suanluang', 'suan luang'],\n ['chomthong', 'chom thong'],\n ['donmueang', 'don mueang'],\n ['donmuang', 'don mueang'],\n ['latphrao', 'lat phrao'],\n ['ladprao', 'lat phrao'],\n ['lardprao', 'lat phrao'],\n ['lat prao', 'lat phrao'],\n ['bangkhae', 'bang khae'],\n ['laksi', 'lak si'],\n ['saimai', 'sai mai'],\n ['khannayao', 'khan na yao'],\n ['saphansung', 'saphan sung'],\n ['wangthonglang', 'wang thonglang'],\n ['khlongsamwa', 'khlong sam wa'],\n ['klongsamwa', 'khlong sam wa'],\n ['bangna', 'bang na'],\n ['thawiwatthana', 'thawi watthana'],\n ['thungkhru', 'thung khru'],\n ['bangbon', 'bang bon'],\n\n // --- Bangkok area / street nicknames with a common misspelling ----------\n ['sathorn', 'sathon'],\n ['wattana', 'watthana'],\n ['silom', 'si lom'],\n\n // --- Provinces and other high-traffic areas ------------------------------\n ['krungthep', 'bangkok'],\n ['korat', 'nakhon ratchasima'],\n ['nakornratchasima', 'nakhon ratchasima'],\n ['chiangmai', 'chiang mai'],\n ['chiangrai', 'chiang rai'],\n ['pathumthani', 'pathum thani'],\n ['patumthani', 'pathum thani'],\n ['samutprakan', 'samut prakan'],\n ['samutsakhon', 'samut sakhon'],\n ['samutsongkhram', 'samut songkhram'],\n ['nakhonpathom', 'nakhon pathom'],\n ['nakhonsawan', 'nakhon sawan'],\n ['nakhonsithammarat', 'nakhon si thammarat'],\n ['prachuapkhirikhan', 'prachuap khiri khan'],\n ['huahin', 'hua hin'],\n ['hatyai', 'hat yai'],\n ['hadyai', 'hat yai'],\n ['had yai', 'hat yai'],\n ['lopburi', 'loburi'],\n ['lop buri', 'loburi'],\n ['buengkan', 'buogkan'],\n ['bungkan', 'buogkan'],\n ['bung kan', 'buogkan'],\n ['udonthani', 'udon thani'],\n ['khonkaen', 'khon kaen'],\n ['suphanburi', 'suphan buri'],\n ['nongkhai', 'nong khai'],\n ['buriram', 'buri ram'],\n])\n\n// --- Layer 2: safe systematic cleanup (dictionary-miss fallback only) -----\n//\n// English administrative words that NEVER occur inside a real `name_en`\n// value (verified against the full dataset) — safe to strip unconditionally.\n// Deliberately excludes \"khet\" and \"tambon\": both occur as genuine name\n// components in the real data (\"Khet Chatuchak\" as a whole amphure name,\n// but also tambons like \"Sanam Chai Khet\" and \"Sam Tambon\"), so stripping\n// them would corrupt otherwise-correct queries.\nconst NOISE_WORDS_RE = /\\b(?:sub[- ]?district|district|province|changwat|amphoe|amphur)\\b/g\nconst WHITESPACE_RE = /\\s+/g\n\nfunction cleanup(text: string): string {\n const stripped = text.replace(NOISE_WORDS_RE, ' ').replace(WHITESPACE_RE, ' ').trim()\n return stripped.length > 0 ? stripped : text\n}\n\nexport function applyRomanizationAliases(normalized: string): string {\n if (!normalized) return normalized\n\n const direct = ALIASES.get(normalized)\n if (direct !== undefined) return direct\n\n const cleaned = cleanup(normalized)\n if (cleaned === normalized) return normalized\n\n const aliasedCleaned = ALIASES.get(cleaned)\n return aliasedCleaned !== undefined ? aliasedCleaned : cleaned\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport { applyRomanizationAliases } from './romanize'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\nconst HAS_LATIN_RE = /[a-z]/i\n\n// Constructing Intl.Collator is expensive; build it once at module load rather\n// than on every comparator invocation inside a hot sort loop.\nconst TH_COLLATOR = new Intl.Collator('th')\n\n/** Plain, allocation-free string comparison. Zip codes in the bundled dataset\n * are fixed-width zero-padded 5-digit strings, so lexical order already\n * matches numeric order — no need for `localeCompare`'s Intl machinery. */\nfunction compareZip(a: string, b: string): number {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\nfunction sortZipMatches(matches: ThaiAddressRecord[], exactZip: string): ThaiAddressRecord[] {\n matches.sort((a, b) => {\n if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1\n if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1\n return compareZip(a.zipCode, b.zipCode)\n })\n return matches\n}\n\n/**\n * Look up records by zip code (exact or prefix match). Exact matches are\n * returned before prefix matches; ties are ordered by ascending zip code.\n *\n * Unlike `searchThaiAddress`, results are NOT capped by `options.limit` —\n * only by `options.zipLimit` (defaults to `Infinity`), since a zip code can\n * legitimately map to dozens of tambons (e.g. `45000` → 33 records).\n */\nexport function lookupByZipCode(\n index: TrigramIndex,\n zip: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n if (!index || !zip) return []\n const normalized = zip.trim()\n if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return []\n\n const zipLimit = options?.zipLimit ?? Infinity\n\n const matches: ThaiAddressRecord[] = []\n for (const [z, indices] of index.zipIndex) {\n if (z.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n sortZipMatches(matches, normalized)\n return matches.slice(0, zipLimit)\n}\n\nfunction rankAgainstName(name: string | undefined, query: string): number {\n if (name === undefined || name.length === 0) return 0\n if (name === query) return 3\n if (name.startsWith(query)) return 2\n if (name.includes(query)) return 1\n return 0\n}\n\n/** `3` = exact tambon name match, `2` = prefix match, `1` = substring match,\n * `0` = no match on the tambon's own (normalized) name. Used to rank a\n * record whose OWN name matches the query above a record that only matches\n * via its parent amphure/province.\n *\n * Both the Thai and the English name are checked: a Latin-script query can\n * never match the Thai name, so without the English side every romanized\n * query would fall back to alphabetical order and reproduce exactly the\n * ranking bug this function exists to fix. */\nfunction computeMatchRank(index: TrigramIndex, idx: number, query: string): number {\n const th = rankAgainstName(index.normTambon?.[idx], query)\n if (th === 3) return 3\n const en = rankAgainstName(index.normTambonEn?.[idx], query)\n return en > th ? en : th\n}\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 // Bound the raw query before running the (multi-pass) normalizer over it.\n if (query.length > 1000) 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 return lookupByZipCode(index, normalized, {\n zipLimit: options?.zipLimit ?? Infinity,\n })\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. The index never contains\n // trigram keys shorter than 3 chars for names of length >= 3, so a sub-3-char\n // query can never usefully match anything.\n if (normalized.length < 3) return []\n\n // Expand common non-RTGS romanizations (e.g. \"lardprao\" -> \"lat phrao\") for\n // Latin-script queries. Thai-script queries are passed through untouched.\n const searchText =\n options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized)\n ? applyRomanizationAliases(normalized)\n : normalized\n\n const queryTrigrams = extractTrigramsNormalized(searchText)\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; matchRank: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) })\n }\n }\n\n // Cheap numeric pre-sort (score, then matchRank) — no collator, no string\n // comparisons. This must include matchRank, not just score: an exact-name\n // match that ties on score with many parent-only matches could otherwise\n // fall outside the re-sorted window below and be lost.\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n return b.matchRank - a.matchRank\n })\n\n // Only the top window is worth the expense of a locale-aware tie-break;\n // re-sort just that slice with the full comparator (including the\n // module-level cached collator) before truncating to `limit`.\n const windowSize = Math.max(limit * 4, 50)\n const window = scored.slice(0, windowSize)\n window.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n if (b.matchRank !== a.matchRank) return b.matchRank - a.matchRank\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) ||\n TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) ||\n TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh)\n )\n })\n\n return window.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type {\n FormatSuggestionOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n} from '../types'\n\nexport function formatThaiAddressSuggestion(\n record: ThaiAddressRecord,\n options?: FormatSuggestionOptions,\n): ThaiAddressSuggestion {\n const labelTh = `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`\n const labelEn = `${record.tambonNameEn} > ${record.amphureNameEn} > ${record.provinceNameEn} ${record.zipCode}`\n\n return {\n id: String(record.tambonId),\n label: options?.locale === 'en' ? labelEn : labelTh,\n labelTh,\n labelEn,\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 * @param options.zipLimit - Maximum results for all-digit (zip code) queries, passed\n * through to `searchThaiAddress`.\n * @param options.initialQuery - Seed value for `query` on first render. Does NOT\n * trigger a search — useful for pre-filling the input with an already-chosen\n * address (e.g. react-hook-form `defaultValues`) without popping the dropdown open.\n * @param options.locale - Locale for the `label` field of returned suggestions.\n * @param options.onSelect - Called with the resolved address after a successful\n * `selectSuggestion`.\n */\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const {\n index,\n limit = 10,\n debounce: debounceMs = 200,\n threshold = 0.4,\n zipLimit,\n initialQuery = '',\n locale,\n onSelect,\n } = 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(initialQuery)\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 // When true, the next run of the debounced query effect is skipped once, then\n // the flag resets so the following real keystroke searches normally. Set by\n // `setQuerySilent` and by the initial-query seed so neither triggers a\n // spurious search / dropdown open.\n const suppressNextSearchRef = useRef(initialQuery.length > 0)\n\n // Both search effects run together on mount (React runs every effect on the\n // first commit regardless of deps). `suppressNextSearchRef` is only consumed\n // once (by whichever effect runs first), so the `[index]` effect additionally\n // no-ops on its very first run — otherwise an `initialQuery` would still fire\n // one immediate synchronous search via this effect even though the debounced\n // effect above correctly skipped its own.\n const isFirstIndexEffectRunRef = useRef(true)\n\n const runSearch = useCallback(\n (q: string, idx: typeof index) => {\n const records = searchThaiAddress(idx, q, { limit, threshold, zipLimit })\n const formatted = records.map((record) => formatThaiAddressSuggestion(record, { locale }))\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 },\n [limit, threshold, zipLimit, locale],\n )\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 if (suppressNextSearchRef.current) {\n suppressNextSearchRef.current = false\n return\n }\n\n timerRef.current = setTimeout(() => {\n runSearch(query, indexRef.current)\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, zipLimit, locale, runSearch])\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 (isFirstIndexEffectRunRef.current) {\n isFirstIndexEffectRunRef.current = false\n return\n }\n if (query.length === 0) return\n if (suppressNextSearchRef.current) {\n suppressNextSearchRef.current = false\n return\n }\n if (timerRef.current) clearTimeout(timerRef.current)\n runSearch(query, index)\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()` or `setQuerySilent()` afterwards if you want to\n * update the input as well without re-opening the dropdown.\n *\n * Returns `null` (instead of throwing) when `item.id` does not match any\n * currently-tracked suggestion — e.g. a stale re-dispatch from an optimistic\n * UI or a re-rendered `Controller`. Callers that previously relied on the\n * thrown error should check for `null`.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress | null => {\n if (timerRef.current) clearTimeout(timerRef.current)\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n return null\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n const resolved = resolveThaiAddress(record)\n onSelect?.(resolved)\n return resolved\n },\n [onSelect],\n )\n\n const clear = useCallback(() => {\n setQuery('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n /**\n * Sets `query` without triggering a search or opening the dropdown. Use this\n * to echo a chosen suggestion's label back into the input after\n * `selectSuggestion`, or to seed the input programmatically at any point\n * after mount (e.g. when async `defaultValues` resolve later).\n */\n const setQuerySilent = useCallback((value: string) => {\n if (timerRef.current) clearTimeout(timerRef.current)\n // Only arm suppression for a non-empty value: the query effect's\n // `query.length === 0` branch returns before ever checking (and\n // resetting) `suppressNextSearchRef`, so arming it here for an empty\n // value would leave it stuck `true` and wrongly swallow the *next* real\n // keystroke.\n if (value.length > 0) suppressNextSearchRef.current = true\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n setQuery(value)\n }, [])\n\n return {\n query,\n setQuery,\n setQuerySilent,\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/romanize.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","/**\n * Romanization alias expansion for Latin-script queries.\n *\n * The bundled dataset indexes only the official RTGS transliteration of each\n * name (`name_en`), so common but non-RTGS spellings a user actually types\n * (\"lardprao\", \"krungthep\", \"sukumvit\") miss the index entirely. This module\n * rewrites such a query into its RTGS form before trigram extraction.\n *\n * Contract:\n * - Input is an ALREADY-NORMALIZED query (lowercased, prefixes and tone marks\n * stripped) as produced by `normalizeThaiAddressText`.\n * - Output is a normalized query too — safe to hand straight to\n * `extractTrigramsNormalized`.\n * - Thai-script input must be returned unchanged.\n * - Must be a pure function; it runs on every keystroke.\n *\n * Design (two layers, cheapest/most-reliable first):\n *\n * 1. A curated dictionary of non-RTGS spelling -> exact RTGS string, as it\n * literally appears in a `name_en` field of the bundled dataset. Targets\n * were read off the real generated index (see the romanize benchmark\n * harness used to build this list), not guessed — a couple of entries\n * intentionally map onto data typos already baked into the dataset\n * (`loburi`, `buogkan`) because that is what is actually indexed.\n *\n * 2. A tiny, conservative cleanup pass used only when the dictionary misses:\n * collapse runs of whitespace, and strip a few English administrative\n * words (`district`, `province`, ...) that never occur inside a real\n * `name_en` value. Both were verified against the full dataset before\n * being added — see the \"rejected rules\" note below for transformations\n * that were tried and thrown out because they collide with real names.\n *\n * Rejected systematic rules (measured against the real dataset, not\n * hypothetical): a blanket `klong` -> `khlong` rewrite would break the real\n * tambons \"Mae Klong\" and \"Ho Klong\"; `muang` -> `mueang` would break ~122\n * real tambons that legitimately contain \"Muang\" as a name component (e.g.\n * \"Bang Muang\", \"Khok Muang\"); `huay` -> `huai` would break the real tambon\n * \"Huay Lueg\"; a blanket hyphen -> space rewrite would break the ~70 real\n * names that use a hyphen on purpose (e.g. \"Bang Pa-in\", the Ayutthaya\n * palace town). None of the classic AUA-vs-RTGS letter swaps (`oo` -> `u`,\n * trailing `-rd` -> `-t`, `dt` -> `t`, `bp` -> `p`, doubled consonants) had\n * any real occurrence in the traffic-prioritized query set used to validate\n * this module, so none were added — see the harness report for the\n * before/after numbers this file was tuned against.\n */\n\n// --- Layer 1: curated alias dictionary -------------------------------------\n//\n// Keys are already-normalized (lowercase) non-RTGS spellings. Values are the\n// exact RTGS substring as it appears in the dataset's `name_en` fields\n// (verified against the real generated index, not guessed).\nconst ALIASES: ReadonlyMap<string, string> = new Map([\n // --- Bangkok khet (districts) — concatenated (\"no space\") forms ---------\n ['phranakhon', 'phra nakhon'],\n ['nongchok', 'nong chok'],\n ['bangrak', 'bang rak'],\n ['bangkhen', 'bang khen'],\n ['bangkapi', 'bang kapi'],\n ['pathumwan', 'pathum wan'],\n ['patumwan', 'pathum wan'],\n ['pomprap', 'pom prap sattru phai'],\n ['phrakhanong', 'phra khanong'],\n ['minburi', 'min buri'],\n ['latkrabang', 'lat krabang'],\n ['ladkrabang', 'lat krabang'],\n ['yannawa', 'yan nawa'],\n ['phayathai', 'phaya thai'],\n ['thonburi', 'thon buri'],\n ['bangkokyai', 'bangkok yai'],\n ['huaikhwang', 'huai khwang'],\n ['huaykhwang', 'huai khwang'],\n ['huaykwang', 'huai khwang'],\n ['khlongsan', 'khlong san'],\n ['klongsan', 'khlong san'],\n ['talingchan', 'taling chan'],\n ['bangkoknoi', 'bangkok noi'],\n ['bangkhunthian', 'bang khun thian'],\n ['phasicharoen', 'phasi charoen'],\n ['nongkhaem', 'nong khaem'],\n ['ratburana', 'rat burana'],\n ['bangphlat', 'bang phlat'],\n ['dindaeng', 'din daeng'],\n ['buengkum', 'bueng kum'],\n ['bangsue', 'bang sue'],\n ['bangkholaem', 'bang kho laem'],\n ['khlongtoei', 'khlong toei'],\n ['klongtoei', 'khlong toei'],\n ['klongtoey', 'khlong toei'],\n ['khlongtoey', 'khlong toei'],\n ['suanluang', 'suan luang'],\n ['chomthong', 'chom thong'],\n ['donmueang', 'don mueang'],\n ['donmuang', 'don mueang'],\n ['latphrao', 'lat phrao'],\n ['ladprao', 'lat phrao'],\n ['lardprao', 'lat phrao'],\n ['lat prao', 'lat phrao'],\n ['bangkhae', 'bang khae'],\n ['laksi', 'lak si'],\n ['saimai', 'sai mai'],\n ['khannayao', 'khan na yao'],\n ['saphansung', 'saphan sung'],\n ['wangthonglang', 'wang thonglang'],\n ['khlongsamwa', 'khlong sam wa'],\n ['klongsamwa', 'khlong sam wa'],\n ['bangna', 'bang na'],\n ['thawiwatthana', 'thawi watthana'],\n ['thungkhru', 'thung khru'],\n ['bangbon', 'bang bon'],\n\n // --- Bangkok area / street nicknames with a common misspelling ----------\n ['sathorn', 'sathon'],\n ['wattana', 'watthana'],\n ['silom', 'si lom'],\n\n // --- Provinces and other high-traffic areas ------------------------------\n ['krungthep', 'bangkok'],\n ['korat', 'nakhon ratchasima'],\n ['nakornratchasima', 'nakhon ratchasima'],\n ['chiangmai', 'chiang mai'],\n ['chiangrai', 'chiang rai'],\n ['pathumthani', 'pathum thani'],\n ['patumthani', 'pathum thani'],\n ['samutprakan', 'samut prakan'],\n ['samutsakhon', 'samut sakhon'],\n ['samutsongkhram', 'samut songkhram'],\n ['nakhonpathom', 'nakhon pathom'],\n ['nakhonsawan', 'nakhon sawan'],\n ['nakhonsithammarat', 'nakhon si thammarat'],\n ['prachuapkhirikhan', 'prachuap khiri khan'],\n ['huahin', 'hua hin'],\n ['hatyai', 'hat yai'],\n ['hadyai', 'hat yai'],\n ['had yai', 'hat yai'],\n ['lopburi', 'loburi'],\n ['lop buri', 'loburi'],\n ['buengkan', 'buogkan'],\n ['bungkan', 'buogkan'],\n ['bung kan', 'buogkan'],\n ['udonthani', 'udon thani'],\n ['khonkaen', 'khon kaen'],\n ['suphanburi', 'suphan buri'],\n ['nongkhai', 'nong khai'],\n ['buriram', 'buri ram'],\n])\n\n// --- Layer 2: safe systematic cleanup (dictionary-miss fallback only) -----\n//\n// English administrative words that NEVER occur inside a real `name_en`\n// value (verified against the full dataset) — safe to strip unconditionally.\n// Deliberately excludes \"khet\" and \"tambon\": both occur as genuine name\n// components in the real data (\"Khet Chatuchak\" as a whole amphure name,\n// but also tambons like \"Sanam Chai Khet\" and \"Sam Tambon\"), so stripping\n// them would corrupt otherwise-correct queries.\nconst NOISE_WORDS_RE = /\\b(?:sub[- ]?district|district|province|changwat|amphoe|amphur)\\b/g\nconst WHITESPACE_RE = /\\s+/g\n\nfunction cleanup(text: string): string {\n const stripped = text.replace(NOISE_WORDS_RE, ' ').replace(WHITESPACE_RE, ' ').trim()\n return stripped.length > 0 ? stripped : text\n}\n\nexport function applyRomanizationAliases(normalized: string): string {\n if (!normalized) return normalized\n\n const direct = ALIASES.get(normalized)\n if (direct !== undefined) return direct\n\n const cleaned = cleanup(normalized)\n if (cleaned === normalized) return normalized\n\n const aliasedCleaned = ALIASES.get(cleaned)\n return aliasedCleaned !== undefined ? aliasedCleaned : cleaned\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport { applyRomanizationAliases } from './romanize'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\nconst HAS_LATIN_RE = /[a-z]/i\n\n// Constructing Intl.Collator is expensive; build it once at module load rather\n// than on every comparator invocation inside a hot sort loop.\nconst TH_COLLATOR = new Intl.Collator('th')\n\n/** Plain, allocation-free string comparison. Zip codes in the bundled dataset\n * are fixed-width zero-padded 5-digit strings, so lexical order already\n * matches numeric order — no need for `localeCompare`'s Intl machinery. */\nfunction compareZip(a: string, b: string): number {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\nfunction sortZipMatches(matches: ThaiAddressRecord[], exactZip: string): ThaiAddressRecord[] {\n matches.sort((a, b) => {\n if (a.zipCode === exactZip && b.zipCode !== exactZip) return -1\n if (b.zipCode === exactZip && a.zipCode !== exactZip) return 1\n return compareZip(a.zipCode, b.zipCode)\n })\n return matches\n}\n\n/**\n * Look up records by zip code (exact or prefix match). Exact matches are\n * returned before prefix matches; ties are ordered by ascending zip code.\n *\n * Unlike `searchThaiAddress`, results are NOT capped by `options.limit` —\n * only by `options.zipLimit` (defaults to `Infinity`), since a zip code can\n * legitimately map to dozens of tambons (e.g. `45000` → 33 records).\n */\nexport function lookupByZipCode(\n index: TrigramIndex,\n zip: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n if (!index || !zip) return []\n const normalized = zip.trim()\n if (!ZIP_CODE_RE.test(normalized) || normalized.length < 2) return []\n\n const zipLimit = options?.zipLimit ?? Infinity\n\n const matches: ThaiAddressRecord[] = []\n for (const [z, indices] of index.zipIndex) {\n if (z.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n sortZipMatches(matches, normalized)\n return matches.slice(0, zipLimit)\n}\n\nfunction rankAgainstName(name: string | undefined, query: string): number {\n if (name === undefined || name.length === 0) return 0\n if (name === query) return 3\n if (name.startsWith(query)) return 2\n if (name.includes(query)) return 1\n return 0\n}\n\n/** `3` = exact tambon name match, `2` = prefix match, `1` = substring match,\n * `0` = no match on the tambon's own (normalized) name. Used to rank a\n * record whose OWN name matches the query above a record that only matches\n * via its parent amphure/province.\n *\n * Both the Thai and the English name are checked: a Latin-script query can\n * never match the Thai name, so without the English side every romanized\n * query would fall back to alphabetical order and reproduce exactly the\n * ranking bug this function exists to fix. */\nfunction computeMatchRank(index: TrigramIndex, idx: number, query: string): number {\n const th = rankAgainstName(index.normTambon?.[idx], query)\n if (th === 3) return 3\n const en = rankAgainstName(index.normTambonEn?.[idx], query)\n return en > th ? en : th\n}\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 // Bound the raw query before running the (multi-pass) normalizer over it.\n if (query.length > 1000) 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 return lookupByZipCode(index, normalized, {\n zipLimit: options?.zipLimit ?? Infinity,\n })\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. The index never contains\n // trigram keys shorter than 3 chars for names of length >= 3, so a sub-3-char\n // query can never usefully match anything.\n if (normalized.length < 3) return []\n\n // Expand common non-RTGS romanizations (e.g. \"lardprao\" -> \"lat phrao\") for\n // Latin-script queries. Thai-script queries are passed through untouched.\n const searchText =\n options?.romanizationAliases !== false && HAS_LATIN_RE.test(normalized)\n ? applyRomanizationAliases(normalized)\n : normalized\n\n const queryTrigrams = extractTrigramsNormalized(searchText)\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; matchRank: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score, matchRank: computeMatchRank(index, idx, searchText) })\n }\n }\n\n // Cheap numeric pre-sort (score, then matchRank) — no collator, no string\n // comparisons. This must include matchRank, not just score: an exact-name\n // match that ties on score with many parent-only matches could otherwise\n // fall outside the re-sorted window below and be lost.\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n return b.matchRank - a.matchRank\n })\n\n // Only the top window is worth the expense of a locale-aware tie-break;\n // re-sort just that slice with the full comparator (including the\n // module-level cached collator) before truncating to `limit`.\n const windowSize = Math.max(limit * 4, 50)\n const window = scored.slice(0, windowSize)\n window.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n if (b.matchRank !== a.matchRank) return b.matchRank - a.matchRank\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n TH_COLLATOR.compare(ra.provinceNameTh, rb.provinceNameTh) ||\n TH_COLLATOR.compare(ra.amphureNameTh, rb.amphureNameTh) ||\n TH_COLLATOR.compare(ra.tambonNameTh, rb.tambonNameTh)\n )\n })\n\n return window.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type {\n FormatSuggestionOptions,\n ThaiAddressRecord,\n ThaiAddressSuggestion,\n} from '../types'\n\nexport function formatThaiAddressSuggestion(\n record: ThaiAddressRecord,\n options?: FormatSuggestionOptions,\n): ThaiAddressSuggestion {\n const labelTh = `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`\n const labelEn = `${record.tambonNameEn} > ${record.amphureNameEn} > ${record.provinceNameEn} ${record.zipCode}`\n\n return {\n id: String(record.tambonId),\n label: options?.locale === 'en' ? labelEn : labelTh,\n labelTh,\n labelEn,\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 * @param options.zipLimit - Maximum results for all-digit (zip code) queries, passed\n * through to `searchThaiAddress`.\n * @param options.initialQuery - Seed value for `query` on first render. Does NOT\n * trigger a search — useful for pre-filling the input with an already-chosen\n * address (e.g. react-hook-form `defaultValues`) without popping the dropdown open.\n * @param options.locale - Locale for the `label` field of returned suggestions.\n * @param options.onSelect - Called with the resolved address after a successful\n * `selectSuggestion`.\n */\nexport function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions) {\n const {\n index,\n limit = 10,\n debounce: debounceMs = 200,\n threshold = 0.4,\n zipLimit,\n initialQuery = '',\n locale,\n onSelect,\n } = 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(initialQuery)\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 // When true, the next run of the debounced query effect is skipped once, then\n // the flag resets so the following real keystroke searches normally. Set by\n // `setQuerySilent` and by the initial-query seed so neither triggers a\n // spurious search / dropdown open.\n const suppressNextSearchRef = useRef(initialQuery.length > 0)\n\n // Both search effects run together on mount (React runs every effect on the\n // first commit regardless of deps). `suppressNextSearchRef` is only consumed\n // once (by whichever effect runs first), so the `[index]` effect additionally\n // no-ops on its very first run — otherwise an `initialQuery` would still fire\n // one immediate synchronous search via this effect even though the debounced\n // effect above correctly skipped its own.\n const isFirstIndexEffectRunRef = useRef(true)\n\n const runSearch = useCallback(\n (q: string, idx: typeof index) => {\n const records = searchThaiAddress(idx, q, { limit, threshold, zipLimit })\n const formatted = records.map((record) => formatThaiAddressSuggestion(record, { locale }))\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 },\n [limit, threshold, zipLimit, locale],\n )\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 if (suppressNextSearchRef.current) {\n suppressNextSearchRef.current = false\n return\n }\n\n timerRef.current = setTimeout(() => {\n runSearch(query, indexRef.current)\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, zipLimit, locale, runSearch])\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 (isFirstIndexEffectRunRef.current) {\n isFirstIndexEffectRunRef.current = false\n return\n }\n if (query.length === 0) return\n if (suppressNextSearchRef.current) {\n suppressNextSearchRef.current = false\n return\n }\n if (timerRef.current) clearTimeout(timerRef.current)\n runSearch(query, index)\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()` or `setQuerySilent()` afterwards if you want to\n * update the input as well without re-opening the dropdown.\n *\n * Returns `null` (instead of throwing) when `item.id` does not match any\n * currently-tracked suggestion — e.g. a stale re-dispatch from an optimistic\n * UI or a re-rendered `Controller`. Callers that previously relied on the\n * thrown error should check for `null`.\n */\n const selectSuggestion = useCallback(\n (item: ThaiAddressSuggestion): ResolvedThaiAddress | null => {\n if (timerRef.current) clearTimeout(timerRef.current)\n const record = matchedRecordsMapRef.current.get(item.id)\n if (!record) {\n return null\n }\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n const resolved = resolveThaiAddress(record)\n onSelect?.(resolved)\n return resolved\n },\n [onSelect],\n )\n\n const clear = useCallback(() => {\n setQuery('')\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n }, [])\n\n /**\n * Sets `query` without triggering a search or opening the dropdown. Use this\n * to echo a chosen suggestion's label back into the input after\n * `selectSuggestion`, or to seed the input programmatically at any point\n * after mount (e.g. when async `defaultValues` resolve later).\n */\n const setQuerySilent = useCallback((value: string) => {\n if (timerRef.current) clearTimeout(timerRef.current)\n // Only arm suppression for a non-empty value: the query effect's\n // `query.length === 0` branch returns before ever checking (and\n // resetting) `suppressNextSearchRef`, so arming it here for an empty\n // value would leave it stuck `true` and wrongly swallow the *next* real\n // keystroke.\n if (value.length > 0) suppressNextSearchRef.current = true\n setSuggestions([])\n matchedRecordsMapRef.current = new Map()\n setQuery(value)\n }, [])\n\n return {\n query,\n setQuery,\n setQuerySilent,\n suggestions,\n isOpen: query.length > 0 && suggestions.length > 0,\n selectSuggestion,\n clear,\n }\n}\n"]}
|