thaizip 0.7.5 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/data.cjs +90 -16
- package/dist/data.js +90 -16
- package/dist/index.cjs +54 -14
- package/dist/index.d.cts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +54 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npm install thaizip
|
|
|
28
28
|
import { loadDefaultIndex } from 'thaizip/data'
|
|
29
29
|
import { searchThaiAddress, formatThaiAddressSuggestion, resolveThaiAddress } from 'thaizip'
|
|
30
30
|
|
|
31
|
-
const index = await loadDefaultIndex() // ~
|
|
31
|
+
const index = await loadDefaultIndex() // ~50ms on the measured device, cached after
|
|
32
32
|
|
|
33
33
|
searchThaiAddress(index, 'ลาดพร้าว')
|
|
34
34
|
searchThaiAddress(index, 'bang rak')
|
|
@@ -39,7 +39,8 @@ formatThaiAddressSuggestion(record, { locale: 'en' })
|
|
|
39
39
|
resolveThaiAddress(record) // for saving
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Call `loadDefaultIndex()` at mount or route load — deferring it to the user's first keystroke
|
|
42
|
+
Call `loadDefaultIndex()` at mount or route load — deferring it to the user's first keystroke can block the UI briefly.
|
|
43
|
+
The cached default index is shared. Ordinary writes to its records and lookup structures throw; build a separate index with `buildThaiAddressIndex` if you need to edit address data. This is protection against accidental changes, not a security boundary against hostile code already running in the same JavaScript realm.
|
|
43
44
|
|
|
44
45
|
## Search
|
|
45
46
|
|
|
@@ -135,9 +136,9 @@ const index = buildThaiAddressIndex({ provinces, amphures, tambons }, {
|
|
|
135
136
|
})
|
|
136
137
|
```
|
|
137
138
|
|
|
138
|
-
Input is validated by default, so a stray non-string field fails with `[thaizip] tambon 100404: expected string for name_th, got number` instead of crashing inside the normalizer.
|
|
139
|
+
Input is validated by default, so a stray non-string field fails with `[thaizip] tambon 100404: expected string for name_th, got number` instead of crashing inside the normalizer. `validateRawData(data)` runs the same checks standalone.
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
Validation also rejects more than 50,000 total rows, names longer than 256 UTF-16 code units, ZIP values longer than 32 code units, or an estimated trigram posting cost above 1,000,000. The estimate counts each province/district's unique trigrams once for every active child subdistrict, so it catches parent-name fan-out. These bounds are suitable for the bundled-size dataset, not a guarantee that arbitrary uploads are safe to process. For larger trusted datasets, pass `{ validate: false }` and apply your own resource limits before building.
|
|
141
142
|
|
|
142
143
|
Types: `RawData`, `RawProvince`, `RawAmphure`, `RawTambon`.
|
|
143
144
|
|
package/dist/data.cjs
CHANGED
|
@@ -35,6 +35,10 @@ function extractTrigramsNormalized(text) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// src/core/indexer.ts
|
|
38
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
39
|
+
var MAX_NAME_LENGTH = 256;
|
|
40
|
+
var MAX_ZIP_LENGTH = 32;
|
|
41
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
38
42
|
function typeErr(table, id, field, expected, value) {
|
|
39
43
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
40
44
|
}
|
|
@@ -55,32 +59,42 @@ function validateRawData(data) {
|
|
|
55
59
|
if (!Array.isArray(tambons)) {
|
|
56
60
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
63
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
64
|
+
}
|
|
65
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
59
66
|
for (let i = 0; i < provinces.length; i++) {
|
|
60
67
|
const p = provinces[i];
|
|
61
68
|
if (p === null || typeof p !== "object") {
|
|
62
69
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
63
70
|
}
|
|
64
71
|
if (typeof p.id !== "number") throw typeErr("province", p.id, "id", "number", p.id);
|
|
65
|
-
if (
|
|
66
|
-
|
|
72
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
73
|
+
provinceById.set(p.id, p);
|
|
67
74
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
68
75
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
76
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
77
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
69
78
|
}
|
|
70
|
-
const
|
|
79
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
71
80
|
for (let i = 0; i < amphures.length; i++) {
|
|
72
81
|
const a = amphures[i];
|
|
73
82
|
if (a === null || typeof a !== "object") {
|
|
74
83
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
75
84
|
}
|
|
76
85
|
if (typeof a.id !== "number") throw typeErr("amphure", a.id, "id", "number", a.id);
|
|
77
|
-
if (
|
|
78
|
-
|
|
86
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
87
|
+
amphureById.set(a.id, a);
|
|
79
88
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
80
89
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
81
90
|
if (typeof a.province_id !== "number") throw typeErr("amphure", a.id, "province_id", "number", a.province_id);
|
|
91
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
92
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
82
93
|
}
|
|
83
94
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
95
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
96
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
97
|
+
let postingBudget = 0;
|
|
84
98
|
for (let i = 0; i < tambons.length; i++) {
|
|
85
99
|
const t = tambons[i];
|
|
86
100
|
if (t === null || typeof t !== "object") {
|
|
@@ -91,11 +105,33 @@ function validateRawData(data) {
|
|
|
91
105
|
seenTambonIds.add(t.id);
|
|
92
106
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
93
107
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
108
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
109
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
94
110
|
const zipType = typeof t.zip_code;
|
|
95
111
|
if (zipType !== "string" && zipType !== "number") {
|
|
96
112
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
97
113
|
}
|
|
98
114
|
if (typeof t.amphure_id !== "number") throw typeErr("tambon", t.id, "amphure_id", "number", t.amphure_id);
|
|
115
|
+
const zipLength = String(t.zip_code).length;
|
|
116
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
117
|
+
if (t.deleted_at) continue;
|
|
118
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
119
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
120
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
121
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
122
|
+
if (provinceGrams === void 0) {
|
|
123
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
124
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
125
|
+
}
|
|
126
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
127
|
+
if (amphureGrams === void 0) {
|
|
128
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
129
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
130
|
+
}
|
|
131
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
132
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
133
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
134
|
+
}
|
|
99
135
|
}
|
|
100
136
|
}
|
|
101
137
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -126,13 +162,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
126
162
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
127
163
|
);
|
|
128
164
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
129
|
-
for (const [id, prov] of provMap) {
|
|
130
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
131
|
-
}
|
|
132
165
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
133
|
-
for (const [id, amp] of ampMap) {
|
|
134
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
135
|
-
}
|
|
136
166
|
const records = [];
|
|
137
167
|
const map = /* @__PURE__ */ new Map();
|
|
138
168
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -182,8 +212,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
182
212
|
normTambonEn.push(normEn);
|
|
183
213
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
184
214
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
185
|
-
|
|
186
|
-
|
|
215
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
216
|
+
if (!provinceGrams) {
|
|
217
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
218
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
219
|
+
}
|
|
220
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
221
|
+
if (!amphureGrams) {
|
|
222
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
223
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
224
|
+
}
|
|
225
|
+
addTrigrams(map, provinceGrams, idx);
|
|
226
|
+
addTrigrams(map, amphureGrams, idx);
|
|
187
227
|
}
|
|
188
228
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
189
229
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
@@ -194,6 +234,40 @@ function buildThaiAddressIndex(data, options) {
|
|
|
194
234
|
var cached = null;
|
|
195
235
|
var inflightPromise = null;
|
|
196
236
|
var loadGeneration = 0;
|
|
237
|
+
function rejectDefaultIndexMutation() {
|
|
238
|
+
throw new TypeError("[thaizip] the shared default index is read-only");
|
|
239
|
+
}
|
|
240
|
+
var readOnlyMapPrototype = Object.create(Map.prototype);
|
|
241
|
+
var readOnlySetPrototype = Object.create(Set.prototype);
|
|
242
|
+
for (const [prototype, methods] of [
|
|
243
|
+
[readOnlyMapPrototype, ["set", "delete", "clear"]],
|
|
244
|
+
[readOnlySetPrototype, ["add", "delete", "clear"]]
|
|
245
|
+
]) {
|
|
246
|
+
for (const method of methods) {
|
|
247
|
+
Object.defineProperty(prototype, method, { value: rejectDefaultIndexMutation });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function lockMapValues(map, lockValue) {
|
|
251
|
+
for (const value of map.values()) lockValue(value);
|
|
252
|
+
Object.setPrototypeOf(map, readOnlyMapPrototype);
|
|
253
|
+
Object.freeze(map);
|
|
254
|
+
}
|
|
255
|
+
function lockDefaultIndex(index) {
|
|
256
|
+
for (const record of index.records) Object.freeze(record);
|
|
257
|
+
Object.freeze(index.records);
|
|
258
|
+
Object.freeze(index.normTambon);
|
|
259
|
+
Object.freeze(index.normTambonEn);
|
|
260
|
+
if (index.sortedZipKeys) Object.freeze(index.sortedZipKeys);
|
|
261
|
+
if (index.sortedZipPostings) Object.freeze(index.sortedZipPostings);
|
|
262
|
+
lockMapValues(index.map, (set) => {
|
|
263
|
+
Object.setPrototypeOf(set, readOnlySetPrototype);
|
|
264
|
+
Object.freeze(set);
|
|
265
|
+
});
|
|
266
|
+
lockMapValues(index.zipIndex, Object.freeze);
|
|
267
|
+
lockMapValues(index.byProvince, Object.freeze);
|
|
268
|
+
lockMapValues(index.byAmphure, Object.freeze);
|
|
269
|
+
return Object.freeze(index);
|
|
270
|
+
}
|
|
197
271
|
function clearDefaultIndex() {
|
|
198
272
|
cached = null;
|
|
199
273
|
inflightPromise = null;
|
|
@@ -207,11 +281,11 @@ async function loadDefaultIndex() {
|
|
|
207
281
|
if (!inflightPromise) {
|
|
208
282
|
const gen = loadGeneration;
|
|
209
283
|
inflightPromise = import('./defaultData.js').then(({ p, a, t }) => {
|
|
210
|
-
const index = buildThaiAddressIndex({
|
|
284
|
+
const index = lockDefaultIndex(buildThaiAddressIndex({
|
|
211
285
|
provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
|
|
212
286
|
amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
|
|
213
287
|
tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
|
|
214
|
-
}, { validate: false });
|
|
288
|
+
}, { validate: false }));
|
|
215
289
|
if (loadGeneration === gen) {
|
|
216
290
|
cached = index;
|
|
217
291
|
inflightPromise = null;
|
package/dist/data.js
CHANGED
|
@@ -33,6 +33,10 @@ function extractTrigramsNormalized(text) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/core/indexer.ts
|
|
36
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
37
|
+
var MAX_NAME_LENGTH = 256;
|
|
38
|
+
var MAX_ZIP_LENGTH = 32;
|
|
39
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
36
40
|
function typeErr(table, id, field, expected, value) {
|
|
37
41
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
38
42
|
}
|
|
@@ -53,32 +57,42 @@ function validateRawData(data) {
|
|
|
53
57
|
if (!Array.isArray(tambons)) {
|
|
54
58
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
61
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
62
|
+
}
|
|
63
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
57
64
|
for (let i = 0; i < provinces.length; i++) {
|
|
58
65
|
const p = provinces[i];
|
|
59
66
|
if (p === null || typeof p !== "object") {
|
|
60
67
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
61
68
|
}
|
|
62
69
|
if (typeof p.id !== "number") throw typeErr("province", p.id, "id", "number", p.id);
|
|
63
|
-
if (
|
|
64
|
-
|
|
70
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
71
|
+
provinceById.set(p.id, p);
|
|
65
72
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
66
73
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
74
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
75
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
67
76
|
}
|
|
68
|
-
const
|
|
77
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
69
78
|
for (let i = 0; i < amphures.length; i++) {
|
|
70
79
|
const a = amphures[i];
|
|
71
80
|
if (a === null || typeof a !== "object") {
|
|
72
81
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
73
82
|
}
|
|
74
83
|
if (typeof a.id !== "number") throw typeErr("amphure", a.id, "id", "number", a.id);
|
|
75
|
-
if (
|
|
76
|
-
|
|
84
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
85
|
+
amphureById.set(a.id, a);
|
|
77
86
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
78
87
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
79
88
|
if (typeof a.province_id !== "number") throw typeErr("amphure", a.id, "province_id", "number", a.province_id);
|
|
89
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
90
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
80
91
|
}
|
|
81
92
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
93
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
94
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
95
|
+
let postingBudget = 0;
|
|
82
96
|
for (let i = 0; i < tambons.length; i++) {
|
|
83
97
|
const t = tambons[i];
|
|
84
98
|
if (t === null || typeof t !== "object") {
|
|
@@ -89,11 +103,33 @@ function validateRawData(data) {
|
|
|
89
103
|
seenTambonIds.add(t.id);
|
|
90
104
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
91
105
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
106
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
107
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
92
108
|
const zipType = typeof t.zip_code;
|
|
93
109
|
if (zipType !== "string" && zipType !== "number") {
|
|
94
110
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
95
111
|
}
|
|
96
112
|
if (typeof t.amphure_id !== "number") throw typeErr("tambon", t.id, "amphure_id", "number", t.amphure_id);
|
|
113
|
+
const zipLength = String(t.zip_code).length;
|
|
114
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
115
|
+
if (t.deleted_at) continue;
|
|
116
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
117
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
118
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
119
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
120
|
+
if (provinceGrams === void 0) {
|
|
121
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
122
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
123
|
+
}
|
|
124
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
125
|
+
if (amphureGrams === void 0) {
|
|
126
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
127
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
128
|
+
}
|
|
129
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
130
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
131
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
132
|
+
}
|
|
97
133
|
}
|
|
98
134
|
}
|
|
99
135
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -124,13 +160,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
124
160
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
125
161
|
);
|
|
126
162
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
127
|
-
for (const [id, prov] of provMap) {
|
|
128
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
129
|
-
}
|
|
130
163
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
131
|
-
for (const [id, amp] of ampMap) {
|
|
132
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
133
|
-
}
|
|
134
164
|
const records = [];
|
|
135
165
|
const map = /* @__PURE__ */ new Map();
|
|
136
166
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -180,8 +210,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
180
210
|
normTambonEn.push(normEn);
|
|
181
211
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
182
212
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
214
|
+
if (!provinceGrams) {
|
|
215
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
216
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
217
|
+
}
|
|
218
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
219
|
+
if (!amphureGrams) {
|
|
220
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
221
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
222
|
+
}
|
|
223
|
+
addTrigrams(map, provinceGrams, idx);
|
|
224
|
+
addTrigrams(map, amphureGrams, idx);
|
|
185
225
|
}
|
|
186
226
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
187
227
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
|
@@ -192,6 +232,40 @@ function buildThaiAddressIndex(data, options) {
|
|
|
192
232
|
var cached = null;
|
|
193
233
|
var inflightPromise = null;
|
|
194
234
|
var loadGeneration = 0;
|
|
235
|
+
function rejectDefaultIndexMutation() {
|
|
236
|
+
throw new TypeError("[thaizip] the shared default index is read-only");
|
|
237
|
+
}
|
|
238
|
+
var readOnlyMapPrototype = Object.create(Map.prototype);
|
|
239
|
+
var readOnlySetPrototype = Object.create(Set.prototype);
|
|
240
|
+
for (const [prototype, methods] of [
|
|
241
|
+
[readOnlyMapPrototype, ["set", "delete", "clear"]],
|
|
242
|
+
[readOnlySetPrototype, ["add", "delete", "clear"]]
|
|
243
|
+
]) {
|
|
244
|
+
for (const method of methods) {
|
|
245
|
+
Object.defineProperty(prototype, method, { value: rejectDefaultIndexMutation });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function lockMapValues(map, lockValue) {
|
|
249
|
+
for (const value of map.values()) lockValue(value);
|
|
250
|
+
Object.setPrototypeOf(map, readOnlyMapPrototype);
|
|
251
|
+
Object.freeze(map);
|
|
252
|
+
}
|
|
253
|
+
function lockDefaultIndex(index) {
|
|
254
|
+
for (const record of index.records) Object.freeze(record);
|
|
255
|
+
Object.freeze(index.records);
|
|
256
|
+
Object.freeze(index.normTambon);
|
|
257
|
+
Object.freeze(index.normTambonEn);
|
|
258
|
+
if (index.sortedZipKeys) Object.freeze(index.sortedZipKeys);
|
|
259
|
+
if (index.sortedZipPostings) Object.freeze(index.sortedZipPostings);
|
|
260
|
+
lockMapValues(index.map, (set) => {
|
|
261
|
+
Object.setPrototypeOf(set, readOnlySetPrototype);
|
|
262
|
+
Object.freeze(set);
|
|
263
|
+
});
|
|
264
|
+
lockMapValues(index.zipIndex, Object.freeze);
|
|
265
|
+
lockMapValues(index.byProvince, Object.freeze);
|
|
266
|
+
lockMapValues(index.byAmphure, Object.freeze);
|
|
267
|
+
return Object.freeze(index);
|
|
268
|
+
}
|
|
195
269
|
function clearDefaultIndex() {
|
|
196
270
|
cached = null;
|
|
197
271
|
inflightPromise = null;
|
|
@@ -205,11 +279,11 @@ async function loadDefaultIndex() {
|
|
|
205
279
|
if (!inflightPromise) {
|
|
206
280
|
const gen = loadGeneration;
|
|
207
281
|
inflightPromise = import('./defaultData.js').then(({ p, a, t }) => {
|
|
208
|
-
const index = buildThaiAddressIndex({
|
|
282
|
+
const index = lockDefaultIndex(buildThaiAddressIndex({
|
|
209
283
|
provinces: p.map(([id, name_th, name_en]) => ({ id, name_th, name_en, geography_id: 0, deleted_at: null })),
|
|
210
284
|
amphures: a.map(([id, name_th, name_en, province_id]) => ({ id, name_th, name_en, province_id, deleted_at: null })),
|
|
211
285
|
tambons: t.map(([id, name_th, name_en, amphure_id, zip_code]) => ({ id, name_th, name_en, amphure_id, zip_code, deleted_at: null }))
|
|
212
|
-
}, { validate: false });
|
|
286
|
+
}, { validate: false }));
|
|
213
287
|
if (loadGeneration === gen) {
|
|
214
288
|
cached = index;
|
|
215
289
|
inflightPromise = null;
|
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,10 @@ function extractTrigramsNormalized(text) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// src/core/indexer.ts
|
|
38
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
39
|
+
var MAX_NAME_LENGTH = 256;
|
|
40
|
+
var MAX_ZIP_LENGTH = 32;
|
|
41
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
38
42
|
function typeErr(table, id, field, expected, value) {
|
|
39
43
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
40
44
|
}
|
|
@@ -55,32 +59,42 @@ function validateRawData(data) {
|
|
|
55
59
|
if (!Array.isArray(tambons)) {
|
|
56
60
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
63
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
64
|
+
}
|
|
65
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
59
66
|
for (let i = 0; i < provinces.length; i++) {
|
|
60
67
|
const p = provinces[i];
|
|
61
68
|
if (p === null || typeof p !== "object") {
|
|
62
69
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
63
70
|
}
|
|
64
71
|
if (typeof p.id !== "number") throw typeErr("province", p.id, "id", "number", p.id);
|
|
65
|
-
if (
|
|
66
|
-
|
|
72
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
73
|
+
provinceById.set(p.id, p);
|
|
67
74
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
68
75
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
76
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
77
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
69
78
|
}
|
|
70
|
-
const
|
|
79
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
71
80
|
for (let i = 0; i < amphures.length; i++) {
|
|
72
81
|
const a = amphures[i];
|
|
73
82
|
if (a === null || typeof a !== "object") {
|
|
74
83
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
75
84
|
}
|
|
76
85
|
if (typeof a.id !== "number") throw typeErr("amphure", a.id, "id", "number", a.id);
|
|
77
|
-
if (
|
|
78
|
-
|
|
86
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
87
|
+
amphureById.set(a.id, a);
|
|
79
88
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
80
89
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
81
90
|
if (typeof a.province_id !== "number") throw typeErr("amphure", a.id, "province_id", "number", a.province_id);
|
|
91
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
92
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
82
93
|
}
|
|
83
94
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
95
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
96
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
97
|
+
let postingBudget = 0;
|
|
84
98
|
for (let i = 0; i < tambons.length; i++) {
|
|
85
99
|
const t = tambons[i];
|
|
86
100
|
if (t === null || typeof t !== "object") {
|
|
@@ -91,11 +105,33 @@ function validateRawData(data) {
|
|
|
91
105
|
seenTambonIds.add(t.id);
|
|
92
106
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
93
107
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
108
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
109
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
94
110
|
const zipType = typeof t.zip_code;
|
|
95
111
|
if (zipType !== "string" && zipType !== "number") {
|
|
96
112
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
97
113
|
}
|
|
98
114
|
if (typeof t.amphure_id !== "number") throw typeErr("tambon", t.id, "amphure_id", "number", t.amphure_id);
|
|
115
|
+
const zipLength = String(t.zip_code).length;
|
|
116
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
117
|
+
if (t.deleted_at) continue;
|
|
118
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
119
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
120
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
121
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
122
|
+
if (provinceGrams === void 0) {
|
|
123
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
124
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
125
|
+
}
|
|
126
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
127
|
+
if (amphureGrams === void 0) {
|
|
128
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
129
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
130
|
+
}
|
|
131
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
132
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
133
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
134
|
+
}
|
|
99
135
|
}
|
|
100
136
|
}
|
|
101
137
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -126,13 +162,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
126
162
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
127
163
|
);
|
|
128
164
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
129
|
-
for (const [id, prov] of provMap) {
|
|
130
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
131
|
-
}
|
|
132
165
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
133
|
-
for (const [id, amp] of ampMap) {
|
|
134
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
135
|
-
}
|
|
136
166
|
const records = [];
|
|
137
167
|
const map = /* @__PURE__ */ new Map();
|
|
138
168
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -182,8 +212,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
182
212
|
normTambonEn.push(normEn);
|
|
183
213
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
184
214
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
185
|
-
|
|
186
|
-
|
|
215
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
216
|
+
if (!provinceGrams) {
|
|
217
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
218
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
219
|
+
}
|
|
220
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
221
|
+
if (!amphureGrams) {
|
|
222
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
223
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
224
|
+
}
|
|
225
|
+
addTrigrams(map, provinceGrams, idx);
|
|
226
|
+
addTrigrams(map, amphureGrams, idx);
|
|
187
227
|
}
|
|
188
228
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
189
229
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|
package/dist/index.d.cts
CHANGED
|
@@ -161,10 +161,9 @@ type RawData = {
|
|
|
161
161
|
* private datasets) who want to fail fast with a descriptive error instead of an
|
|
162
162
|
* opaque crash deep inside the normalizer, or silent `"undefined"` labels.
|
|
163
163
|
*
|
|
164
|
-
* Throws a `TypeError`
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* provided.
|
|
164
|
+
* Throws a `TypeError` for bad shapes or duplicate IDs and a `RangeError` for
|
|
165
|
+
* resource limits (all prefixed `[thaizip]`). Does not filter soft-deleted rows
|
|
166
|
+
* before shape checks — validates every row as provided.
|
|
168
167
|
*
|
|
169
168
|
* IDs must be unique within each table (duplicates throw, they are not silently
|
|
170
169
|
* discarded — two suggestions sharing an id would resolve to the wrong record).
|
package/dist/index.d.ts
CHANGED
|
@@ -161,10 +161,9 @@ type RawData = {
|
|
|
161
161
|
* private datasets) who want to fail fast with a descriptive error instead of an
|
|
162
162
|
* opaque crash deep inside the normalizer, or silent `"undefined"` labels.
|
|
163
163
|
*
|
|
164
|
-
* Throws a `TypeError`
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* provided.
|
|
164
|
+
* Throws a `TypeError` for bad shapes or duplicate IDs and a `RangeError` for
|
|
165
|
+
* resource limits (all prefixed `[thaizip]`). Does not filter soft-deleted rows
|
|
166
|
+
* before shape checks — validates every row as provided.
|
|
168
167
|
*
|
|
169
168
|
* IDs must be unique within each table (duplicates throw, they are not silently
|
|
170
169
|
* discarded — two suggestions sharing an id would resolve to the wrong record).
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,10 @@ function extractTrigramsNormalized(text) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/core/indexer.ts
|
|
36
|
+
var MAX_TOTAL_ROWS = 5e4;
|
|
37
|
+
var MAX_NAME_LENGTH = 256;
|
|
38
|
+
var MAX_ZIP_LENGTH = 32;
|
|
39
|
+
var MAX_POSTING_BUDGET = 1e6;
|
|
36
40
|
function typeErr(table, id, field, expected, value) {
|
|
37
41
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
38
42
|
}
|
|
@@ -53,32 +57,42 @@ function validateRawData(data) {
|
|
|
53
57
|
if (!Array.isArray(tambons)) {
|
|
54
58
|
throw new TypeError(`[thaizip] RawData.tambons must be an array, got ${typeName(tambons)}`);
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
if (provinces.length + amphures.length + tambons.length > MAX_TOTAL_ROWS) {
|
|
61
|
+
throw new RangeError(`[thaizip] RawData row limit exceeded (${MAX_TOTAL_ROWS})`);
|
|
62
|
+
}
|
|
63
|
+
const provinceById = /* @__PURE__ */ new Map();
|
|
57
64
|
for (let i = 0; i < provinces.length; i++) {
|
|
58
65
|
const p = provinces[i];
|
|
59
66
|
if (p === null || typeof p !== "object") {
|
|
60
67
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
61
68
|
}
|
|
62
69
|
if (typeof p.id !== "number") throw typeErr("province", p.id, "id", "number", p.id);
|
|
63
|
-
if (
|
|
64
|
-
|
|
70
|
+
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
71
|
+
provinceById.set(p.id, p);
|
|
65
72
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
66
73
|
if (typeof p.name_en !== "string") throw typeErr("province", p.id, "name_en", "string", p.name_en);
|
|
74
|
+
if (p.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_th too long`);
|
|
75
|
+
if (p.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] province ${p.id}: name_en too long`);
|
|
67
76
|
}
|
|
68
|
-
const
|
|
77
|
+
const amphureById = /* @__PURE__ */ new Map();
|
|
69
78
|
for (let i = 0; i < amphures.length; i++) {
|
|
70
79
|
const a = amphures[i];
|
|
71
80
|
if (a === null || typeof a !== "object") {
|
|
72
81
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
73
82
|
}
|
|
74
83
|
if (typeof a.id !== "number") throw typeErr("amphure", a.id, "id", "number", a.id);
|
|
75
|
-
if (
|
|
76
|
-
|
|
84
|
+
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
85
|
+
amphureById.set(a.id, a);
|
|
77
86
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
78
87
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
79
88
|
if (typeof a.province_id !== "number") throw typeErr("amphure", a.id, "province_id", "number", a.province_id);
|
|
89
|
+
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
90
|
+
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
80
91
|
}
|
|
81
92
|
const seenTambonIds = /* @__PURE__ */ new Set();
|
|
93
|
+
const provinceTrigramCounts = /* @__PURE__ */ new Map();
|
|
94
|
+
const amphureTrigramCounts = /* @__PURE__ */ new Map();
|
|
95
|
+
let postingBudget = 0;
|
|
82
96
|
for (let i = 0; i < tambons.length; i++) {
|
|
83
97
|
const t = tambons[i];
|
|
84
98
|
if (t === null || typeof t !== "object") {
|
|
@@ -89,11 +103,33 @@ function validateRawData(data) {
|
|
|
89
103
|
seenTambonIds.add(t.id);
|
|
90
104
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
91
105
|
if (typeof t.name_en !== "string") throw typeErr("tambon", t.id, "name_en", "string", t.name_en);
|
|
106
|
+
if (t.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_th too long`);
|
|
107
|
+
if (t.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: name_en too long`);
|
|
92
108
|
const zipType = typeof t.zip_code;
|
|
93
109
|
if (zipType !== "string" && zipType !== "number") {
|
|
94
110
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
95
111
|
}
|
|
96
112
|
if (typeof t.amphure_id !== "number") throw typeErr("tambon", t.id, "amphure_id", "number", t.amphure_id);
|
|
113
|
+
const zipLength = String(t.zip_code).length;
|
|
114
|
+
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
115
|
+
if (t.deleted_at) continue;
|
|
116
|
+
const amphure = amphureById.get(t.amphure_id);
|
|
117
|
+
const province = amphure && provinceById.get(amphure.province_id);
|
|
118
|
+
if (!amphure || amphure.deleted_at || !province || province.deleted_at) continue;
|
|
119
|
+
let provinceGrams = provinceTrigramCounts.get(province.id);
|
|
120
|
+
if (provinceGrams === void 0) {
|
|
121
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en).size;
|
|
122
|
+
provinceTrigramCounts.set(province.id, provinceGrams);
|
|
123
|
+
}
|
|
124
|
+
let amphureGrams = amphureTrigramCounts.get(amphure.id);
|
|
125
|
+
if (amphureGrams === void 0) {
|
|
126
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en).size;
|
|
127
|
+
amphureTrigramCounts.set(amphure.id, amphureGrams);
|
|
128
|
+
}
|
|
129
|
+
postingBudget += t.name_th.length + t.name_en.length + zipLength + provinceGrams + amphureGrams;
|
|
130
|
+
if (postingBudget > MAX_POSTING_BUDGET) {
|
|
131
|
+
throw new RangeError(`[thaizip] estimated trigram posting budget exceeded (${MAX_POSTING_BUDGET})`);
|
|
132
|
+
}
|
|
97
133
|
}
|
|
98
134
|
}
|
|
99
135
|
function addTrigrams(map, trigrams, idx) {
|
|
@@ -124,13 +160,7 @@ function buildThaiAddressIndex(data, options) {
|
|
|
124
160
|
amphures.filter((a) => !a.deleted_at).map((a) => [a.id, a])
|
|
125
161
|
);
|
|
126
162
|
const provTrigrams = /* @__PURE__ */ new Map();
|
|
127
|
-
for (const [id, prov] of provMap) {
|
|
128
|
-
provTrigrams.set(id, combinedTrigrams(prov.name_th, prov.name_en));
|
|
129
|
-
}
|
|
130
163
|
const ampTrigrams = /* @__PURE__ */ new Map();
|
|
131
|
-
for (const [id, amp] of ampMap) {
|
|
132
|
-
ampTrigrams.set(id, combinedTrigrams(amp.name_th, amp.name_en));
|
|
133
|
-
}
|
|
134
164
|
const records = [];
|
|
135
165
|
const map = /* @__PURE__ */ new Map();
|
|
136
166
|
const zipIndex = /* @__PURE__ */ new Map();
|
|
@@ -180,8 +210,18 @@ function buildThaiAddressIndex(data, options) {
|
|
|
180
210
|
normTambonEn.push(normEn);
|
|
181
211
|
addTrigrams(map, extractTrigramsNormalized(normEn), idx);
|
|
182
212
|
addTrigrams(map, extractTrigrams(record.zipCode), idx);
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
let provinceGrams = provTrigrams.get(province.id);
|
|
214
|
+
if (!provinceGrams) {
|
|
215
|
+
provinceGrams = combinedTrigrams(province.name_th, province.name_en);
|
|
216
|
+
provTrigrams.set(province.id, provinceGrams);
|
|
217
|
+
}
|
|
218
|
+
let amphureGrams = ampTrigrams.get(amphure.id);
|
|
219
|
+
if (!amphureGrams) {
|
|
220
|
+
amphureGrams = combinedTrigrams(amphure.name_th, amphure.name_en);
|
|
221
|
+
ampTrigrams.set(amphure.id, amphureGrams);
|
|
222
|
+
}
|
|
223
|
+
addTrigrams(map, provinceGrams, idx);
|
|
224
|
+
addTrigrams(map, amphureGrams, idx);
|
|
185
225
|
}
|
|
186
226
|
const sortedZipKeys = [...zipIndex.keys()].sort();
|
|
187
227
|
const sortedZipPostings = sortedZipKeys.map((z) => zipIndex.get(z));
|