thaizip 0.7.4 → 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 +6 -5
- package/dist/data.cjs +126 -79
- package/dist/data.d.cts +14 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.js +126 -80
- package/dist/defaultData.js +6 -0
- 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/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));
|