thaizip 0.6.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -14,10 +14,37 @@ type TrigramIndex = {
14
14
  map: Map<string, Set<number>>;
15
15
  records: ThaiAddressRecord[];
16
16
  zipIndex: Map<string, number[]>;
17
+ /**
18
+ * Normalized Thai tambon names, parallel to `records` (same index).
19
+ * Used by the search ranker for exact/prefix match detection without
20
+ * re-normalizing on every query.
21
+ */
22
+ normTambon: string[];
23
+ /**
24
+ * Normalized English (RTGS) tambon names, parallel to `records`. Needed so
25
+ * romanized queries get the same own-name-beats-parent-name ranking that
26
+ * Thai queries get.
27
+ */
28
+ normTambonEn: string[];
29
+ /** Record indices grouped by `provinceId`, in insertion order. */
30
+ byProvince: Map<number, number[]>;
31
+ /** Record indices grouped by `amphureId`, in insertion order. */
32
+ byAmphure: Map<number, number[]>;
33
+ };
34
+ /** Which language a formatted label should be rendered in. */
35
+ type AddressLocale = 'th' | 'en';
36
+ type FormatSuggestionOptions = {
37
+ /** Language for the `label` field. Defaults to `'th'`. */
38
+ locale?: AddressLocale;
17
39
  };
18
40
  type ThaiAddressSuggestion = {
19
41
  id: string;
42
+ /** Human-readable label in the requested locale (Thai by default). */
20
43
  label: string;
44
+ /** Human-readable label in Thai, regardless of the requested locale. */
45
+ labelTh: string;
46
+ /** Human-readable label in English, regardless of the requested locale. */
47
+ labelEn: string;
21
48
  tambon: string;
22
49
  tambonEn: string;
23
50
  amphure: string;
@@ -26,6 +53,27 @@ type ThaiAddressSuggestion = {
26
53
  provinceEn: string;
27
54
  zipCode: string;
28
55
  };
56
+ /** A province entry returned by the enumeration API. */
57
+ type ProvinceSummary = {
58
+ id: number;
59
+ nameTh: string;
60
+ nameEn: string;
61
+ };
62
+ /** An amphure (district) entry returned by the enumeration API. */
63
+ type AmphureSummary = {
64
+ id: number;
65
+ nameTh: string;
66
+ nameEn: string;
67
+ provinceId: number;
68
+ };
69
+ /** A tambon (subdistrict) entry returned by the enumeration API. */
70
+ type TambonSummary = {
71
+ id: number;
72
+ nameTh: string;
73
+ nameEn: string;
74
+ amphureId: number;
75
+ zipCode: string;
76
+ };
29
77
  type ResolvedThaiAddress = {
30
78
  tambon: string;
31
79
  tambonEn: string;
@@ -43,6 +91,17 @@ type ResolvedThaiAddress = {
43
91
  type SearchOptions = {
44
92
  limit?: number;
45
93
  threshold?: number;
94
+ /**
95
+ * Maximum results for all-digit (zip code) queries. A zip code maps to at most
96
+ * a few dozen tambons, so this defaults to `Infinity` — capping zip results at
97
+ * the text-autocomplete `limit` silently hides valid subdistricts.
98
+ */
99
+ zipLimit?: number;
100
+ /**
101
+ * Expand common romanization variants (e.g. `lardprao` → `lat phrao`) before
102
+ * searching. Only affects Latin-script queries. Defaults to `true`.
103
+ */
104
+ romanizationAliases?: boolean;
46
105
  };
47
106
  type RawGeography = {
48
107
  id: number;
@@ -73,6 +132,12 @@ type RawTambon = {
73
132
  };
74
133
  type BuildIndexOptions = {
75
134
  onSkip?: (tambon: RawTambon) => void;
135
+ /**
136
+ * Validate that every consumed field has the expected runtime type and throw a
137
+ * descriptive `TypeError` if not. Defaults to `true`; set to `false` to skip
138
+ * the checks when the data source is already trusted (e.g. the bundled index).
139
+ */
140
+ validate?: boolean;
76
141
  };
77
142
  type RawData = {
78
143
  geographies?: RawGeography[];
@@ -81,14 +146,103 @@ type RawData = {
81
146
  tambons: RawTambon[];
82
147
  };
83
148
 
149
+ /**
150
+ * Validate that a `RawData` payload has the runtime shapes `buildThaiAddressIndex`
151
+ * expects. Intended for consumers building an index from their own data (CSV, CMS,
152
+ * private datasets) who want to fail fast with a descriptive error instead of an
153
+ * opaque crash deep inside the normalizer, or silent `"undefined"` labels.
154
+ *
155
+ * Throws a `TypeError` (message prefixed `[thaizip]`) naming the table, the
156
+ * offending record's `id`, the field, and the actual `typeof` on the first bad
157
+ * field found. Does not filter soft-deleted rows first — validates every row as
158
+ * provided.
159
+ */
160
+ declare function validateRawData(data: RawData): void;
84
161
  declare function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex;
85
162
 
163
+ /**
164
+ * Look up records by zip code (exact or prefix match). Exact matches are
165
+ * returned before prefix matches; ties are ordered by ascending zip code.
166
+ *
167
+ * Unlike `searchThaiAddress`, results are NOT capped by `options.limit` —
168
+ * only by `options.zipLimit` (defaults to `Infinity`), since a zip code can
169
+ * legitimately map to dozens of tambons (e.g. `45000` → 33 records).
170
+ */
171
+ declare function lookupByZipCode(index: TrigramIndex, zip: string, options?: SearchOptions): ThaiAddressRecord[];
86
172
  declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
87
173
 
88
- declare function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion;
174
+ /**
175
+ * All provinces present in the index, deduplicated and sorted by Thai name.
176
+ * Backed by `index.byProvince` (O(unique provinces)); falls back to scanning
177
+ * `index.records` if an older-shaped index lacks that map.
178
+ */
179
+ declare function listProvinces(index: TrigramIndex): ProvinceSummary[];
180
+ /**
181
+ * Amphures (districts) within a given province, deduplicated and sorted by
182
+ * Thai name. Unknown `provinceId` returns `[]`. Backed by `index.byProvince`;
183
+ * falls back to scanning `index.records` if an older-shaped index lacks it.
184
+ */
185
+ declare function listAmphures(index: TrigramIndex, provinceId: number): AmphureSummary[];
186
+ /**
187
+ * Tambons (subdistricts) within a given amphure, sorted by Thai name.
188
+ * Unknown `amphureId` returns `[]`. Backed by `index.byAmphure`; falls back
189
+ * to scanning `index.records` if an older-shaped index lacks it. Each
190
+ * `byAmphure` entry already maps to exactly one tambon record, so no
191
+ * deduplication is needed here.
192
+ */
193
+ declare function listTambons(index: TrigramIndex, amphureId: number): TambonSummary[];
194
+
195
+ declare function formatThaiAddressSuggestion(record: ThaiAddressRecord, options?: FormatSuggestionOptions): ThaiAddressSuggestion;
89
196
 
90
197
  declare function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress;
91
198
 
92
199
  declare function normalizeThaiAddressText(input: string): string;
93
200
 
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 };
201
+ /**
202
+ * Romanization alias expansion for Latin-script queries.
203
+ *
204
+ * The bundled dataset indexes only the official RTGS transliteration of each
205
+ * name (`name_en`), so common but non-RTGS spellings a user actually types
206
+ * ("lardprao", "krungthep", "sukumvit") miss the index entirely. This module
207
+ * rewrites such a query into its RTGS form before trigram extraction.
208
+ *
209
+ * Contract:
210
+ * - Input is an ALREADY-NORMALIZED query (lowercased, prefixes and tone marks
211
+ * stripped) as produced by `normalizeThaiAddressText`.
212
+ * - Output is a normalized query too — safe to hand straight to
213
+ * `extractTrigramsNormalized`.
214
+ * - Thai-script input must be returned unchanged.
215
+ * - Must be a pure function; it runs on every keystroke.
216
+ *
217
+ * Design (two layers, cheapest/most-reliable first):
218
+ *
219
+ * 1. A curated dictionary of non-RTGS spelling -> exact RTGS string, as it
220
+ * literally appears in a `name_en` field of the bundled dataset. Targets
221
+ * were read off the real generated index (see the romanize benchmark
222
+ * harness used to build this list), not guessed — a couple of entries
223
+ * intentionally map onto data typos already baked into the dataset
224
+ * (`loburi`, `buogkan`) because that is what is actually indexed.
225
+ *
226
+ * 2. A tiny, conservative cleanup pass used only when the dictionary misses:
227
+ * collapse runs of whitespace, and strip a few English administrative
228
+ * words (`district`, `province`, ...) that never occur inside a real
229
+ * `name_en` value. Both were verified against the full dataset before
230
+ * being added — see the "rejected rules" note below for transformations
231
+ * that were tried and thrown out because they collide with real names.
232
+ *
233
+ * Rejected systematic rules (measured against the real dataset, not
234
+ * hypothetical): a blanket `klong` -> `khlong` rewrite would break the real
235
+ * tambons "Mae Klong" and "Ho Klong"; `muang` -> `mueang` would break ~122
236
+ * real tambons that legitimately contain "Muang" as a name component (e.g.
237
+ * "Bang Muang", "Khok Muang"); `huay` -> `huai` would break the real tambon
238
+ * "Huay Lueg"; a blanket hyphen -> space rewrite would break the ~70 real
239
+ * names that use a hyphen on purpose (e.g. "Bang Pa-in", the Ayutthaya
240
+ * palace town). None of the classic AUA-vs-RTGS letter swaps (`oo` -> `u`,
241
+ * trailing `-rd` -> `-t`, `dt` -> `t`, `bp` -> `p`, doubled consonants) had
242
+ * any real occurrence in the traffic-prioritized query set used to validate
243
+ * this module, so none were added — see the harness report for the
244
+ * before/after numbers this file was tuned against.
245
+ */
246
+ declare function applyRomanizationAliases(normalized: string): string;
247
+
248
+ export { type AddressLocale, type AmphureSummary, type BuildIndexOptions, type FormatSuggestionOptions, type ProvinceSummary, type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type TambonSummary, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, applyRomanizationAliases, buildThaiAddressIndex, formatThaiAddressSuggestion, listAmphures, listProvinces, listTambons, lookupByZipCode, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, validateRawData };
package/dist/index.d.ts CHANGED
@@ -14,10 +14,37 @@ type TrigramIndex = {
14
14
  map: Map<string, Set<number>>;
15
15
  records: ThaiAddressRecord[];
16
16
  zipIndex: Map<string, number[]>;
17
+ /**
18
+ * Normalized Thai tambon names, parallel to `records` (same index).
19
+ * Used by the search ranker for exact/prefix match detection without
20
+ * re-normalizing on every query.
21
+ */
22
+ normTambon: string[];
23
+ /**
24
+ * Normalized English (RTGS) tambon names, parallel to `records`. Needed so
25
+ * romanized queries get the same own-name-beats-parent-name ranking that
26
+ * Thai queries get.
27
+ */
28
+ normTambonEn: string[];
29
+ /** Record indices grouped by `provinceId`, in insertion order. */
30
+ byProvince: Map<number, number[]>;
31
+ /** Record indices grouped by `amphureId`, in insertion order. */
32
+ byAmphure: Map<number, number[]>;
33
+ };
34
+ /** Which language a formatted label should be rendered in. */
35
+ type AddressLocale = 'th' | 'en';
36
+ type FormatSuggestionOptions = {
37
+ /** Language for the `label` field. Defaults to `'th'`. */
38
+ locale?: AddressLocale;
17
39
  };
18
40
  type ThaiAddressSuggestion = {
19
41
  id: string;
42
+ /** Human-readable label in the requested locale (Thai by default). */
20
43
  label: string;
44
+ /** Human-readable label in Thai, regardless of the requested locale. */
45
+ labelTh: string;
46
+ /** Human-readable label in English, regardless of the requested locale. */
47
+ labelEn: string;
21
48
  tambon: string;
22
49
  tambonEn: string;
23
50
  amphure: string;
@@ -26,6 +53,27 @@ type ThaiAddressSuggestion = {
26
53
  provinceEn: string;
27
54
  zipCode: string;
28
55
  };
56
+ /** A province entry returned by the enumeration API. */
57
+ type ProvinceSummary = {
58
+ id: number;
59
+ nameTh: string;
60
+ nameEn: string;
61
+ };
62
+ /** An amphure (district) entry returned by the enumeration API. */
63
+ type AmphureSummary = {
64
+ id: number;
65
+ nameTh: string;
66
+ nameEn: string;
67
+ provinceId: number;
68
+ };
69
+ /** A tambon (subdistrict) entry returned by the enumeration API. */
70
+ type TambonSummary = {
71
+ id: number;
72
+ nameTh: string;
73
+ nameEn: string;
74
+ amphureId: number;
75
+ zipCode: string;
76
+ };
29
77
  type ResolvedThaiAddress = {
30
78
  tambon: string;
31
79
  tambonEn: string;
@@ -43,6 +91,17 @@ type ResolvedThaiAddress = {
43
91
  type SearchOptions = {
44
92
  limit?: number;
45
93
  threshold?: number;
94
+ /**
95
+ * Maximum results for all-digit (zip code) queries. A zip code maps to at most
96
+ * a few dozen tambons, so this defaults to `Infinity` — capping zip results at
97
+ * the text-autocomplete `limit` silently hides valid subdistricts.
98
+ */
99
+ zipLimit?: number;
100
+ /**
101
+ * Expand common romanization variants (e.g. `lardprao` → `lat phrao`) before
102
+ * searching. Only affects Latin-script queries. Defaults to `true`.
103
+ */
104
+ romanizationAliases?: boolean;
46
105
  };
47
106
  type RawGeography = {
48
107
  id: number;
@@ -73,6 +132,12 @@ type RawTambon = {
73
132
  };
74
133
  type BuildIndexOptions = {
75
134
  onSkip?: (tambon: RawTambon) => void;
135
+ /**
136
+ * Validate that every consumed field has the expected runtime type and throw a
137
+ * descriptive `TypeError` if not. Defaults to `true`; set to `false` to skip
138
+ * the checks when the data source is already trusted (e.g. the bundled index).
139
+ */
140
+ validate?: boolean;
76
141
  };
77
142
  type RawData = {
78
143
  geographies?: RawGeography[];
@@ -81,14 +146,103 @@ type RawData = {
81
146
  tambons: RawTambon[];
82
147
  };
83
148
 
149
+ /**
150
+ * Validate that a `RawData` payload has the runtime shapes `buildThaiAddressIndex`
151
+ * expects. Intended for consumers building an index from their own data (CSV, CMS,
152
+ * private datasets) who want to fail fast with a descriptive error instead of an
153
+ * opaque crash deep inside the normalizer, or silent `"undefined"` labels.
154
+ *
155
+ * Throws a `TypeError` (message prefixed `[thaizip]`) naming the table, the
156
+ * offending record's `id`, the field, and the actual `typeof` on the first bad
157
+ * field found. Does not filter soft-deleted rows first — validates every row as
158
+ * provided.
159
+ */
160
+ declare function validateRawData(data: RawData): void;
84
161
  declare function buildThaiAddressIndex(data: RawData, options?: BuildIndexOptions): TrigramIndex;
85
162
 
163
+ /**
164
+ * Look up records by zip code (exact or prefix match). Exact matches are
165
+ * returned before prefix matches; ties are ordered by ascending zip code.
166
+ *
167
+ * Unlike `searchThaiAddress`, results are NOT capped by `options.limit` —
168
+ * only by `options.zipLimit` (defaults to `Infinity`), since a zip code can
169
+ * legitimately map to dozens of tambons (e.g. `45000` → 33 records).
170
+ */
171
+ declare function lookupByZipCode(index: TrigramIndex, zip: string, options?: SearchOptions): ThaiAddressRecord[];
86
172
  declare function searchThaiAddress(index: TrigramIndex, query: string, options?: SearchOptions): ThaiAddressRecord[];
87
173
 
88
- declare function formatThaiAddressSuggestion(record: ThaiAddressRecord): ThaiAddressSuggestion;
174
+ /**
175
+ * All provinces present in the index, deduplicated and sorted by Thai name.
176
+ * Backed by `index.byProvince` (O(unique provinces)); falls back to scanning
177
+ * `index.records` if an older-shaped index lacks that map.
178
+ */
179
+ declare function listProvinces(index: TrigramIndex): ProvinceSummary[];
180
+ /**
181
+ * Amphures (districts) within a given province, deduplicated and sorted by
182
+ * Thai name. Unknown `provinceId` returns `[]`. Backed by `index.byProvince`;
183
+ * falls back to scanning `index.records` if an older-shaped index lacks it.
184
+ */
185
+ declare function listAmphures(index: TrigramIndex, provinceId: number): AmphureSummary[];
186
+ /**
187
+ * Tambons (subdistricts) within a given amphure, sorted by Thai name.
188
+ * Unknown `amphureId` returns `[]`. Backed by `index.byAmphure`; falls back
189
+ * to scanning `index.records` if an older-shaped index lacks it. Each
190
+ * `byAmphure` entry already maps to exactly one tambon record, so no
191
+ * deduplication is needed here.
192
+ */
193
+ declare function listTambons(index: TrigramIndex, amphureId: number): TambonSummary[];
194
+
195
+ declare function formatThaiAddressSuggestion(record: ThaiAddressRecord, options?: FormatSuggestionOptions): ThaiAddressSuggestion;
89
196
 
90
197
  declare function resolveThaiAddress(record: ThaiAddressRecord): ResolvedThaiAddress;
91
198
 
92
199
  declare function normalizeThaiAddressText(input: string): string;
93
200
 
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 };
201
+ /**
202
+ * Romanization alias expansion for Latin-script queries.
203
+ *
204
+ * The bundled dataset indexes only the official RTGS transliteration of each
205
+ * name (`name_en`), so common but non-RTGS spellings a user actually types
206
+ * ("lardprao", "krungthep", "sukumvit") miss the index entirely. This module
207
+ * rewrites such a query into its RTGS form before trigram extraction.
208
+ *
209
+ * Contract:
210
+ * - Input is an ALREADY-NORMALIZED query (lowercased, prefixes and tone marks
211
+ * stripped) as produced by `normalizeThaiAddressText`.
212
+ * - Output is a normalized query too — safe to hand straight to
213
+ * `extractTrigramsNormalized`.
214
+ * - Thai-script input must be returned unchanged.
215
+ * - Must be a pure function; it runs on every keystroke.
216
+ *
217
+ * Design (two layers, cheapest/most-reliable first):
218
+ *
219
+ * 1. A curated dictionary of non-RTGS spelling -> exact RTGS string, as it
220
+ * literally appears in a `name_en` field of the bundled dataset. Targets
221
+ * were read off the real generated index (see the romanize benchmark
222
+ * harness used to build this list), not guessed — a couple of entries
223
+ * intentionally map onto data typos already baked into the dataset
224
+ * (`loburi`, `buogkan`) because that is what is actually indexed.
225
+ *
226
+ * 2. A tiny, conservative cleanup pass used only when the dictionary misses:
227
+ * collapse runs of whitespace, and strip a few English administrative
228
+ * words (`district`, `province`, ...) that never occur inside a real
229
+ * `name_en` value. Both were verified against the full dataset before
230
+ * being added — see the "rejected rules" note below for transformations
231
+ * that were tried and thrown out because they collide with real names.
232
+ *
233
+ * Rejected systematic rules (measured against the real dataset, not
234
+ * hypothetical): a blanket `klong` -> `khlong` rewrite would break the real
235
+ * tambons "Mae Klong" and "Ho Klong"; `muang` -> `mueang` would break ~122
236
+ * real tambons that legitimately contain "Muang" as a name component (e.g.
237
+ * "Bang Muang", "Khok Muang"); `huay` -> `huai` would break the real tambon
238
+ * "Huay Lueg"; a blanket hyphen -> space rewrite would break the ~70 real
239
+ * names that use a hyphen on purpose (e.g. "Bang Pa-in", the Ayutthaya
240
+ * palace town). None of the classic AUA-vs-RTGS letter swaps (`oo` -> `u`,
241
+ * trailing `-rd` -> `-t`, `dt` -> `t`, `bp` -> `p`, doubled consonants) had
242
+ * any real occurrence in the traffic-prioritized query set used to validate
243
+ * this module, so none were added — see the harness report for the
244
+ * before/after numbers this file was tuned against.
245
+ */
246
+ declare function applyRomanizationAliases(normalized: string): string;
247
+
248
+ export { type AddressLocale, type AmphureSummary, type BuildIndexOptions, type FormatSuggestionOptions, type ProvinceSummary, type RawAmphure, type RawData, type RawGeography, type RawProvince, type RawTambon, type ResolvedThaiAddress, type SearchOptions, type TambonSummary, type ThaiAddressRecord, type ThaiAddressSuggestion, type TrigramIndex, applyRomanizationAliases, buildThaiAddressIndex, formatThaiAddressSuggestion, listAmphures, listProvinces, listTambons, lookupByZipCode, normalizeThaiAddressText, resolveThaiAddress, searchThaiAddress, validateRawData };