thaizip 0.5.1 → 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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/data/loader.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, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n console.warn(\n `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): ` +\n `amphure_id=${tambon.amphure_id} not found or deleted`\n )\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n 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, zipIndex }\n}\n","import { buildThaiAddressIndex } from '../core/indexer'\nimport type { TrigramIndex } from '../types'\nimport type { CompactProvince, CompactAmphure, CompactTambon } from './defaultData'\n\nlet cached: TrigramIndex | null = null\n\nexport function clearDefaultIndex(): void {\n cached = null\n}\n\nexport async function loadDefaultIndex(): Promise<TrigramIndex> {\n if (cached) return cached\n const { p, a, t } = await import('./defaultData') as {\n p: CompactProvince[]\n a: CompactAmphure[]\n t: CompactTambon[]\n }\n cached = buildThaiAddressIndex({\n provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),\n amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),\n tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code: Number(zip_code), deleted_at: null })),\n })\n return cached\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)\n })\n return matches.slice(0, limit)\n }\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) ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh) ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh)\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\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 const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\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 matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\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 }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\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 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 setQueryState('')\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"]}
1
+ {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { BuildIndexOptions, RawData, RawTambon, ThaiAddressRecord, TrigramIndex } from '../types'\n\nfunction addTrigrams(map: Map<string, Set<number>>, trigrams: Set<string>, idx: number): void {\n for (const trigram of trigrams) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n}\n\nfunction combinedTrigrams(nameTh: string, nameEn: string): Set<string> {\n const s = new Set<string>()\n for (const t of extractTrigrams(nameTh)) s.add(t)\n for (const t of extractTrigrams(nameEn)) s.add(t)\n return s\n}\n\nexport function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n // Pre-compute trigrams for shared province and amphure fields to avoid\n // redundant normalization when multiple tambons share the same parent.\n const provTrigrams = new Map<number, Set<string>>()\n for (const [id, prov] of provMap) {\n provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en))\n }\n const ampTrigrams = new Map<number, Set<string>>()\n for (const [id, amp] of ampMap) {\n ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en))\n }\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n // Tambon-specific fields (unique per record)\n addTrigrams(map, extractTrigrams(record.tambonNameTh), idx)\n addTrigrams(map, extractTrigrams(record.tambonNameEn), idx)\n addTrigrams(map, extractTrigrams(record.zipCode), idx)\n // Reuse pre-computed province and amphure trigrams\n addTrigrams(map, provTrigrams.get(province.id)!, idx)\n addTrigrams(map, ampTrigrams.get(amphure.id)!, idx)\n }\n\n return { map, records, zipIndex }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -44,12 +44,6 @@ type SearchOptions = {
44
44
  limit?: number;
45
45
  threshold?: number;
46
46
  };
47
- type UseThaiAddressAutocompleteOptions = {
48
- index: TrigramIndex;
49
- limit?: number;
50
- debounce?: number;
51
- threshold?: number;
52
- };
53
47
  type RawGeography = {
54
48
  id: number;
55
49
  name: string;
@@ -71,12 +65,15 @@ type RawAmphure = {
71
65
  };
72
66
  type RawTambon = {
73
67
  id: number;
74
- zip_code: number;
68
+ zip_code: number | string;
75
69
  name_th: string;
76
70
  name_en: string;
77
71
  amphure_id: number;
78
72
  deleted_at: string | null;
79
73
  };
74
+ type BuildIndexOptions = {
75
+ onSkip?: (tambon: RawTambon) => void;
76
+ };
80
77
  type RawData = {
81
78
  geographies?: RawGeography[];
82
79
  provinces: RawProvince[];
@@ -84,9 +81,7 @@ type RawData = {
84
81
  tambons: RawTambon[];
85
82
  };
86
83
 
87
- declare function buildThaiAddressIndex(data: RawData): TrigramIndex;
88
-
89
- declare function clearDefaultIndex(): void;
84
+ declare function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex;
90
85
 
91
86
  declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
92
87
 
@@ -96,13 +91,4 @@ declare function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddr
96
91
 
97
92
  declare function normalizeThaiAddressText(input: string): string;
98
93
 
99
- declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions): {
100
- query: string;
101
- setQuery: (value: string) => void;
102
- suggestions: ThaiAddressSuggestion[];
103
- isOpen: boolean;
104
- selectSuggestion: (item: ThaiAddressSuggestion) => ResolvedThaiAddress;
105
- clear: () => void;
106
- };
107
-
108
- export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, clearDefaultIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
94
+ export { type BuildIndexOptions, type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress };
package/dist/index.d.ts CHANGED
@@ -44,12 +44,6 @@ type SearchOptions = {
44
44
  limit?: number;
45
45
  threshold?: number;
46
46
  };
47
- type UseThaiAddressAutocompleteOptions = {
48
- index: TrigramIndex;
49
- limit?: number;
50
- debounce?: number;
51
- threshold?: number;
52
- };
53
47
  type RawGeography = {
54
48
  id: number;
55
49
  name: string;
@@ -71,12 +65,15 @@ type RawAmphure = {
71
65
  };
72
66
  type RawTambon = {
73
67
  id: number;
74
- zip_code: number;
68
+ zip_code: number | string;
75
69
  name_th: string;
76
70
  name_en: string;
77
71
  amphure_id: number;
78
72
  deleted_at: string | null;
79
73
  };
74
+ type BuildIndexOptions = {
75
+ onSkip?: (tambon: RawTambon) => void;
76
+ };
80
77
  type RawData = {
81
78
  geographies?: RawGeography[];
82
79
  provinces: RawProvince[];
@@ -84,9 +81,7 @@ type RawData = {
84
81
  tambons: RawTambon[];
85
82
  };
86
83
 
87
- declare function buildThaiAddressIndex(data: RawData): TrigramIndex;
88
-
89
- declare function clearDefaultIndex(): void;
84
+ declare function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex;
90
85
 
91
86
  declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
92
87
 
@@ -96,13 +91,4 @@ declare function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddr
96
91
 
97
92
  declare function normalizeThaiAddressText(input: string): string;
98
93
 
99
- declare function useThaiAddressAutocomplete(options: UseThaiAddressAutocompleteOptions): {
100
- query: string;
101
- setQuery: (value: string) => void;
102
- suggestions: ThaiAddressSuggestion[];
103
- isOpen: boolean;
104
- selectSuggestion: (item: ThaiAddressSuggestion) => ResolvedThaiAddress;
105
- clear: () => void;
106
- };
107
-
108
- export { type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, type UseThaiAddressAutocompleteOptions, buildThaiAddressIndex, clearDefaultIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
94
+ export { type BuildIndexOptions, type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress };
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
- import { useState, useRef, useCallback, useEffect } from 'react';
2
-
3
1
  // src/core/normalizer.ts
4
- var TONE_MARKS = /[\u0E48-\u0E4B\u0E47\u0E4C]/g;
5
- var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต)/;
2
+ var TONE_MARKS = /[่-๋็์]/g;
3
+ var ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\.|อ\.|ต\.|ข\.)/;
6
4
  function normalizeThaiAddressText(input) {
7
5
  if (!input) return "";
8
6
  let text = input.trim();
@@ -30,7 +28,23 @@ function extractTrigramsNormalized(text) {
30
28
  }
31
29
 
32
30
  // src/core/indexer.ts
33
- function buildThaiAddressIndex(data) {
31
+ function addTrigrams(map, trigrams, idx) {
32
+ for (const trigram of trigrams) {
33
+ let set = map.get(trigram);
34
+ if (!set) {
35
+ set = /* @__PURE__ */ new Set();
36
+ map.set(trigram, set);
37
+ }
38
+ set.add(idx);
39
+ }
40
+ }
41
+ function combinedTrigrams(nameTh, nameEn) {
42
+ const s = /* @__PURE__ */ new Set();
43
+ for (const t of extractTrigrams(nameTh)) s.add(t);
44
+ for (const t of extractTrigrams(nameEn)) s.add(t);
45
+ return s;
46
+ }
47
+ function buildThaiAddressIndex(data, options) {
34
48
  const { provinces, amphures, tambons } = data;
35
49
  const provMap = new Map(
36
50
  provinces.filter((p) => !p.deleted_at).map((p) => [p.id, p])
@@ -38,6 +52,14 @@ function buildThaiAddressIndex(data) {
38
52
  const ampMap = new Map(
39
53
  amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
40
54
  );
55
+ const provTrigrams = /* @__PURE__ */ new Map();
56
+ for (const [id, prov] of provMap) {
57
+ provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
58
+ }
59
+ const ampTrigrams = /* @__PURE__ */ new Map();
60
+ for (const [id, amp] of ampMap) {
61
+ ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
62
+ }
41
63
  const records = [];
42
64
  const map = /* @__PURE__ */ new Map();
43
65
  const zipIndex = /* @__PURE__ */ new Map();
@@ -45,13 +67,14 @@ function buildThaiAddressIndex(data) {
45
67
  if (tambon.deleted_at) continue;
46
68
  const amphure = ampMap.get(tambon.amphure_id);
47
69
  if (!amphure) {
48
- console.warn(
49
- `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): amphure_id=${tambon.amphure_id} not found or deleted`
50
- );
70
+ if (options?.onSkip) options.onSkip(tambon);
51
71
  continue;
52
72
  }
53
73
  const province = provMap.get(amphure.province_id);
54
- if (!province) continue;
74
+ if (!province) {
75
+ if (options?.onSkip) options.onSkip(tambon);
76
+ continue;
77
+ }
55
78
  const record = {
56
79
  provinceId: province.id,
57
80
  provinceNameTh: province.name_th,
@@ -69,30 +92,14 @@ function buildThaiAddressIndex(data) {
69
92
  const existing = zipIndex.get(record.zipCode);
70
93
  if (existing) existing.push(idx);
71
94
  else zipIndex.set(record.zipCode, [idx]);
72
- const fields = [
73
- record.tambonNameTh,
74
- record.tambonNameEn,
75
- record.amphureNameTh,
76
- record.amphureNameEn,
77
- record.provinceNameTh,
78
- record.provinceNameEn,
79
- record.zipCode
80
- ];
81
- for (const field of fields) {
82
- for (const trigram of extractTrigrams(field)) {
83
- let set = map.get(trigram);
84
- if (!set) {
85
- set = /* @__PURE__ */ new Set();
86
- map.set(trigram, set);
87
- }
88
- set.add(idx);
89
- }
90
- }
95
+ addTrigrams(map, extractTrigrams(record.tambonNameTh), idx);
96
+ addTrigrams(map, extractTrigrams(record.tambonNameEn), idx);
97
+ addTrigrams(map, extractTrigrams(record.zipCode), idx);
98
+ addTrigrams(map, provTrigrams.get(province.id), idx);
99
+ addTrigrams(map, ampTrigrams.get(amphure.id), idx);
91
100
  }
92
101
  return { map, records, zipIndex };
93
102
  }
94
- function clearDefaultIndex() {
95
- }
96
103
 
97
104
  // src/core/search.ts
98
105
  var ZIP_CODE_RE = /^\d+$/;
@@ -114,10 +121,11 @@ function searchThaiAddress(index, query, options) {
114
121
  matches.sort((a, b) => {
115
122
  if (a.zipCode === normalized && b.zipCode !== normalized) return -1;
116
123
  if (b.zipCode === normalized && a.zipCode !== normalized) return 1;
117
- return a.zipCode.localeCompare(b.zipCode);
124
+ return a.zipCode.localeCompare(b.zipCode, void 0, { numeric: true });
118
125
  });
119
126
  return matches.slice(0, limit);
120
127
  }
128
+ if (normalized.length < 3 && query.trim().length < 3) return [];
121
129
  const queryTrigrams = extractTrigramsNormalized(normalized);
122
130
  const hits = /* @__PURE__ */ new Map();
123
131
  for (const trigram of queryTrigrams) {
@@ -138,7 +146,7 @@ function searchThaiAddress(index, query, options) {
138
146
  if (b.score !== a.score) return b.score - a.score;
139
147
  const ra = index.records[a.idx];
140
148
  const rb = index.records[b.idx];
141
- return ra.provinceNameTh.localeCompare(rb.provinceNameTh) || ra.amphureNameTh.localeCompare(rb.amphureNameTh) || ra.tambonNameTh.localeCompare(rb.tambonNameTh);
149
+ return ra.provinceNameTh.localeCompare(rb.provinceNameTh, "th") || ra.amphureNameTh.localeCompare(rb.amphureNameTh, "th") || ra.tambonNameTh.localeCompare(rb.tambonNameTh, "th");
142
150
  });
143
151
  return scored.slice(0, limit).map(({ idx }) => index.records[idx]);
144
152
  }
@@ -175,63 +183,7 @@ function resolveThaiAddress(record) {
175
183
  postalCode: record.zipCode
176
184
  };
177
185
  }
178
- function useThaiAddressAutocomplete(options) {
179
- const { index, limit = 10, debounce: debounceMs = 200, threshold = 0.4 } = options;
180
- const [query, setQueryState] = useState("");
181
- const [suggestions, setSuggestions] = useState([]);
182
- const timerRef = useRef(null);
183
- const matchedRecordsMapRef = useRef(/* @__PURE__ */ new Map());
184
- const setQuery = useCallback((value) => {
185
- setQueryState(value);
186
- }, []);
187
- useEffect(() => {
188
- if (timerRef.current) clearTimeout(timerRef.current);
189
- if (query.length === 0) {
190
- setSuggestions([]);
191
- matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
192
- return;
193
- }
194
- timerRef.current = setTimeout(() => {
195
- const records = searchThaiAddress(index, query, { limit, threshold });
196
- const formatted = records.map(formatThaiAddressSuggestion);
197
- const recordMap = /* @__PURE__ */ new Map();
198
- for (let i = 0; i < records.length; i++) {
199
- recordMap.set(formatted[i].id, records[i]);
200
- }
201
- matchedRecordsMapRef.current = recordMap;
202
- setSuggestions(formatted);
203
- }, debounceMs);
204
- return () => {
205
- if (timerRef.current) clearTimeout(timerRef.current);
206
- };
207
- }, [query, index, limit, threshold, debounceMs]);
208
- const selectSuggestion = useCallback(
209
- (item) => {
210
- const record = matchedRecordsMapRef.current.get(item.id);
211
- if (!record) {
212
- throw new Error(`[thaizip] No record found for suggestion id "${item.id}". Make sure to use suggestions returned by this hook.`);
213
- }
214
- setSuggestions([]);
215
- matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
216
- return resolveThaiAddress(record);
217
- },
218
- []
219
- );
220
- const clear = useCallback(() => {
221
- setQueryState("");
222
- setSuggestions([]);
223
- matchedRecordsMapRef.current = /* @__PURE__ */ new Map();
224
- }, []);
225
- return {
226
- query,
227
- setQuery,
228
- suggestions,
229
- isOpen: query.length > 0 && suggestions.length > 0,
230
- selectSuggestion,
231
- clear
232
- };
233
- }
234
186
 
235
- export { buildThaiAddressIndex, clearDefaultIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, useThaiAddressAutocomplete };
187
+ export { buildThaiAddressIndex, formatThaiAddressSuggestion, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress };
236
188
  //# sourceMappingURL=index.js.map
237
189
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/data/loader.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, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { RawData, ThaiAddressRecord, TrigramIndex } from '../types'\n\nexport function buildThaiAddressIndex(data: RawData): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n console.warn(\n `[thaizip] skipping tambon id=${tambon.id} (${tambon.name_th}): ` +\n `amphure_id=${tambon.amphure_id} not found or deleted`\n )\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) continue\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n 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, zipIndex }\n}\n","import { buildThaiAddressIndex } from '../core/indexer'\nimport type { TrigramIndex } from '../types'\nimport type { CompactProvince, CompactAmphure, CompactTambon } from './defaultData'\n\nlet cached: TrigramIndex | null = null\n\nexport function clearDefaultIndex(): void {\n cached = null\n}\n\nexport async function loadDefaultIndex(): Promise<TrigramIndex> {\n if (cached) return cached\n const { p, a, t } = await import('./defaultData') as {\n p: CompactProvince[]\n a: CompactAmphure[]\n t: CompactTambon[]\n }\n cached = buildThaiAddressIndex({\n provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),\n amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),\n tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code: Number(zip_code), deleted_at: null })),\n })\n return cached\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)\n })\n return matches.slice(0, limit)\n }\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) ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh) ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh)\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\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 const matchedRecordsMapRef = useRef<Map<string, ThaiAddressRecord>>(new Map())\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 matchedRecordsMapRef.current = new Map()\n return\n }\n\n timerRef.current = setTimeout(() => {\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 }, debounceMs)\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current)\n }\n }, [query, index, limit, threshold, debounceMs])\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 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 setQueryState('')\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"]}
1
+ {"version":3,"sources":["../src/core/normalizer.ts","../src/core/trigrams.ts","../src/core/indexer.ts","../src/core/search.ts","../src/core/formatter.ts","../src/core/resolver.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["const TONE_MARKS = /[่-๋็์]/g\nconst ADDRESS_PREFIXES = /^(จังหวัด|อำเภอ|ตำบล|แขวง|เขต|จ\\.|อ\\.|ต\\.|ข\\.)/\n\nexport function normalizeThaiAddressText(input: string): string {\n if (!input) return ''\n let text = input.trim()\n text = text.replace(ADDRESS_PREFIXES, '').trim()\n text = text.replace(TONE_MARKS, '')\n text = text.toLowerCase()\n return text\n}\n","import { normalizeThaiAddressText } from './normalizer'\n\n// Public: for callers who start from raw (un-normalised) text — e.g. indexer\nexport function extractTrigrams(text: string): Set<string> {\n return extractTrigramsNormalized(normalizeThaiAddressText(text))\n}\n\n// Internal: for callers who already hold a normalised string — e.g. search.ts\nexport function extractTrigramsNormalized(text: string): Set<string> {\n const result = new Set<string>()\n if (text.length === 0) return result\n if (text.length < 3) {\n result.add(text)\n return result\n }\n for (let i = 0; i <= text.length - 3; i++) {\n result.add(text.slice(i, i + 3))\n }\n return result\n}\n","import { extractTrigrams } from './trigrams'\nimport type { BuildIndexOptions, RawData, RawTambon, ThaiAddressRecord, TrigramIndex } from '../types'\n\nfunction addTrigrams(map: Map<string, Set<number>>, trigrams: Set<string>, idx: number): void {\n for (const trigram of trigrams) {\n let set = map.get(trigram)\n if (!set) {\n set = new Set()\n map.set(trigram, set)\n }\n set.add(idx)\n }\n}\n\nfunction combinedTrigrams(nameTh: string, nameEn: string): Set<string> {\n const s = new Set<string>()\n for (const t of extractTrigrams(nameTh)) s.add(t)\n for (const t of extractTrigrams(nameEn)) s.add(t)\n return s\n}\n\nexport function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex {\n const { provinces, amphures, tambons } = data\n\n // Build lookup maps (filter deleted)\n const provMap = new Map(\n provinces.filter(p => !p.deleted_at).map(p => [p.id, p])\n )\n const ampMap = new Map(\n amphures.filter(a => !a.deleted_at).map(a => [a.id, a])\n )\n\n // Pre-compute trigrams for shared province and amphure fields to avoid\n // redundant normalization when multiple tambons share the same parent.\n const provTrigrams = new Map<number, Set<string>>()\n for (const [id, prov] of provMap) {\n provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en))\n }\n const ampTrigrams = new Map<number, Set<string>>()\n for (const [id, amp] of ampMap) {\n ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en))\n }\n\n const records: ThaiAddressRecord[] = []\n const map = new Map<string, Set<number>>()\n const zipIndex = new Map<string, number[]>()\n\n for (const tambon of tambons) {\n if (tambon.deleted_at) continue\n\n const amphure = ampMap.get(tambon.amphure_id)\n if (!amphure) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const province = provMap.get(amphure.province_id)\n if (!province) {\n if (options?.onSkip) options.onSkip(tambon as RawTambon)\n continue\n }\n\n const record: ThaiAddressRecord = {\n provinceId: province.id,\n provinceNameTh: province.name_th,\n provinceNameEn: province.name_en,\n amphureId: amphure.id,\n amphureNameTh: amphure.name_th,\n amphureNameEn: amphure.name_en,\n tambonId: tambon.id,\n tambonNameTh: tambon.name_th,\n tambonNameEn: tambon.name_en,\n zipCode: String(tambon.zip_code),\n }\n\n const idx = records.length\n records.push(record)\n\n // Build zip index\n const existing = zipIndex.get(record.zipCode)\n if (existing) existing.push(idx)\n else zipIndex.set(record.zipCode, [idx])\n\n // Tambon-specific fields (unique per record)\n addTrigrams(map, extractTrigrams(record.tambonNameTh), idx)\n addTrigrams(map, extractTrigrams(record.tambonNameEn), idx)\n addTrigrams(map, extractTrigrams(record.zipCode), idx)\n // Reuse pre-computed province and amphure trigrams\n addTrigrams(map, provTrigrams.get(province.id)!, idx)\n addTrigrams(map, ampTrigrams.get(amphure.id)!, idx)\n }\n\n return { map, records, zipIndex }\n}\n","import { normalizeThaiAddressText } from './normalizer'\nimport { extractTrigramsNormalized } from './trigrams'\nimport type { SearchOptions, ThaiAddressRecord, TrigramIndex } from '../types'\n\nconst ZIP_CODE_RE = /^\\d+$/\n\nexport function searchThaiAddress(\n index: TrigramIndex,\n query: string,\n options?: SearchOptions,\n): ThaiAddressRecord[] {\n const limit = options?.limit ?? 10\n const threshold = options?.threshold ?? 0.4\n\n if (!index || !query) return []\n const normalized = normalizeThaiAddressText(query)\n if (normalized.length === 0) return []\n if (normalized.length > 300) return []\n\n // Special case: zip code query (require at least 2 digits to avoid overly broad matches)\n if (ZIP_CODE_RE.test(normalized)) {\n if (normalized.length < 2) return []\n const matches: ThaiAddressRecord[] = []\n for (const [zip, indices] of index.zipIndex) {\n if (zip.startsWith(normalized)) {\n for (const idx of indices) matches.push(index.records[idx])\n }\n }\n // exact match first, then ascending zip code\n matches.sort((a, b) => {\n if (a.zipCode === normalized && b.zipCode !== normalized) return -1\n if (b.zipCode === normalized && a.zipCode !== normalized) return 1\n return a.zipCode.localeCompare(b.zipCode, undefined, { numeric: true })\n })\n return matches.slice(0, limit)\n }\n\n // Queries shorter than 3 chars produce a single-element trigram set where every\n // partial hit scores 1.0 — meaningless for text search.\n // Exception: if the raw query was longer (e.g. \"ต.กา\" normalizes to \"กา\"),\n // a Thai address prefix was stripped — allow through to avoid silent empty results.\n if (normalized.length < 3 && query.trim().length < 3) return []\n\n const queryTrigrams = extractTrigramsNormalized(normalized)\n\n // Accumulate hit counts per record index\n const hits = new Map<number, number>()\n for (const trigram of queryTrigrams) {\n const candidates = index.map.get(trigram)\n if (!candidates) continue\n for (const idx of candidates) {\n hits.set(idx, (hits.get(idx) ?? 0) + 1)\n }\n }\n\n const scored: { idx: number; score: number }[] = []\n for (const [idx, count] of hits) {\n const score = count / queryTrigrams.size\n if (score >= threshold) {\n scored.push({ idx, score })\n }\n }\n\n scored.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score\n const ra = index.records[a.idx]\n const rb = index.records[b.idx]\n return (\n ra.provinceNameTh.localeCompare(rb.provinceNameTh, 'th') ||\n ra.amphureNameTh.localeCompare(rb.amphureNameTh, 'th') ||\n ra.tambonNameTh.localeCompare(rb.tambonNameTh, 'th')\n )\n })\n\n return scored.slice(0, limit).map(({ idx }) => index.records[idx])\n}\n","import type { ThaiAddressRecord, ThaiAddressSuggestion } from '../types'\n\nexport function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion {\n return {\n id: String(record.tambonId),\n label: `${record.tambonNameTh} > ${record.amphureNameTh} > ${record.provinceNameTh} ${record.zipCode}`,\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n }\n}\n","import type { ThaiAddressRecord, ResolvedThaiAddress } from '../types'\n\nexport function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress {\n return {\n tambon: record.tambonNameTh,\n tambonEn: record.tambonNameEn,\n amphure: record.amphureNameTh,\n amphureEn: record.amphureNameEn,\n province: record.provinceNameTh,\n provinceEn: record.provinceNameEn,\n zipCode: record.zipCode,\n subdistrict: record.tambonNameTh,\n subdistrictEn: record.tambonNameEn,\n district: record.amphureNameTh,\n districtEn: record.amphureNameEn,\n postalCode: record.zipCode,\n }\n}\n"]}