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/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
- const seenProvinceIds = /* @__PURE__ */ new Set();
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 (seenProvinceIds.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
66
- seenProvinceIds.add(p.id);
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 seenAmphureIds = /* @__PURE__ */ new Set();
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 (seenAmphureIds.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
78
- seenAmphureIds.add(a.id);
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
- addTrigrams(map, provTrigrams.get(province.id), idx);
186
- addTrigrams(map, ampTrigrams.get(amphure.id), idx);
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` (message prefixed `[thaizip]`) naming the table, the
165
- * offending record's `id`, the field, and the actual `typeof` on the first bad
166
- * field found. Does not filter soft-deleted rows first — validates every row as
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` (message prefixed `[thaizip]`) naming the table, the
165
- * offending record's `id`, the field, and the actual `typeof` on the first bad
166
- * field found. Does not filter soft-deleted rows first — validates every row as
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
- const seenProvinceIds = /* @__PURE__ */ new Set();
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 (seenProvinceIds.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
64
- seenProvinceIds.add(p.id);
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 seenAmphureIds = /* @__PURE__ */ new Set();
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 (seenAmphureIds.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
76
- seenAmphureIds.add(a.id);
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
- addTrigrams(map, provTrigrams.get(province.id), idx);
184
- addTrigrams(map, ampTrigrams.get(amphure.id), idx);
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thaizip",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Fast, fuzzy Thai address autocomplete library (headless + React)",
5
5
  "keywords": [
6
6
  "thai",