@0xd33p/vietnam-divisions 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +80 -0
  2. package/dist/__tests__/vn.test.d.ts +2 -0
  3. package/dist/__tests__/vn.test.d.ts.map +1 -0
  4. package/dist/__tests__/vn.test.js +1107 -0
  5. package/dist/__tests__/vn.test.js.map +1 -0
  6. package/dist/data/legacy/commune.json +11077 -0
  7. package/dist/data/legacy/district.json +730 -0
  8. package/dist/data/legacy/province.json +65 -0
  9. package/dist/data/v3/commune.json +16607 -0
  10. package/dist/data/v3/province.json +240 -0
  11. package/dist/data/v3/province_merges.json +256 -0
  12. package/dist/data/v3/ward_mappings.json +98795 -0
  13. package/dist/index.d.ts +62 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +332 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/internal/address.d.ts +37 -0
  18. package/dist/internal/address.d.ts.map +1 -0
  19. package/dist/internal/address.js +164 -0
  20. package/dist/internal/address.js.map +1 -0
  21. package/dist/internal/helpers.d.ts +24 -0
  22. package/dist/internal/helpers.d.ts.map +1 -0
  23. package/dist/internal/helpers.js +55 -0
  24. package/dist/internal/helpers.js.map +1 -0
  25. package/dist/internal/legacy/index.d.ts +21 -0
  26. package/dist/internal/legacy/index.d.ts.map +1 -0
  27. package/dist/internal/legacy/index.js +60 -0
  28. package/dist/internal/legacy/index.js.map +1 -0
  29. package/dist/internal/legacy/types.d.ts +18 -0
  30. package/dist/internal/legacy/types.d.ts.map +1 -0
  31. package/dist/internal/legacy/types.js +2 -0
  32. package/dist/internal/legacy/types.js.map +1 -0
  33. package/dist/internal/v3/index.d.ts +21 -0
  34. package/dist/internal/v3/index.d.ts.map +1 -0
  35. package/dist/internal/v3/index.js +55 -0
  36. package/dist/internal/v3/index.js.map +1 -0
  37. package/dist/internal/v3/types.d.ts +33 -0
  38. package/dist/internal/v3/types.d.ts.map +1 -0
  39. package/dist/internal/v3/types.js +2 -0
  40. package/dist/internal/v3/types.js.map +1 -0
  41. package/dist/types.d.ts +37 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +2 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +32 -0
@@ -0,0 +1,62 @@
1
+ import type { GeoUnit, GeoResult, GeoErrorResult, AddressLookupResult } from './types.js';
2
+ export type { GeoUnit, GeoError, GeoResult, AddressLookupResult, } from './types.js';
3
+ export declare const vn: {
4
+ /**
5
+ * Get administrative divisions.
6
+ * - No arguments → 34 provinces (v3 system)
7
+ * - With parentId → communes/wards under that province (v3 system)
8
+ */
9
+ divisions(parentId?: string): GeoResult<GeoUnit[]> | GeoErrorResult;
10
+ /**
11
+ * Look up a single unit by ID, code, or name.
12
+ * Auto-detects: province ID (2-char like "79"), commune ID (5-char like "00004"),
13
+ * province code ("HCM", "HNI"), or full name.
14
+ * Auto-resolves: old province IDs via merge/mapping data.
15
+ */
16
+ lookup(query: string): GeoResult<GeoUnit & {
17
+ kind: "province" | "ward";
18
+ }> | GeoErrorResult;
19
+ /**
20
+ * Search divisions by name (case-insensitive, partial match).
21
+ * - Without parentId → search in provinces
22
+ * - With parentId → search in communes of that province
23
+ */
24
+ search(name: string, parentId?: string): GeoResult<GeoUnit[]>;
25
+ /**
26
+ * Look up a full address string in both legacy and v3 systems.
27
+ *
28
+ * Parses a comma-separated address (e.g. "Xã Đông Hòa, TP Thái Bình, Thái Bình"
29
+ * or "Dong Hoa, Thai Binh"), resolves province/district/commune in the legacy
30
+ * system (63 tỉnh), then cross-references to the v3 system (34 tỉnh).
31
+ *
32
+ * **Strict mode (Option B):** Must find exactly one province — fails with
33
+ * `address_missing_province` or `address_ambiguous_province` otherwise.
34
+ */
35
+ addressLookup(raw: string): GeoResult<AddressLookupResult> | GeoErrorResult;
36
+ /**
37
+ * Legacy system (63 provinces, 3 levels: province → district → commune).
38
+ * Explicit opt-in for backward compatibility.
39
+ */
40
+ legacy: {
41
+ /**
42
+ * Get legacy divisions.
43
+ * - No arguments → 63 provinces
44
+ * - With parentId → districts under that province
45
+ */
46
+ divisions(parentId?: string): GeoResult<GeoUnit[]> | GeoErrorResult;
47
+ /**
48
+ * Look up a legacy unit by ID.
49
+ * Auto-detects: province ID (2-char), district ID (3-char), commune ID (5-char).
50
+ */
51
+ lookup(query: string): GeoResult<GeoUnit & {
52
+ kind: "province" | "district" | "ward";
53
+ }> | GeoErrorResult;
54
+ /**
55
+ * Search legacy divisions by name.
56
+ * - Without parentId → search in provinces
57
+ * - With parentId → search in districts of that province
58
+ */
59
+ search(name: string, parentId?: string): GeoResult<GeoUnit[]>;
60
+ };
61
+ };
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,cAAc,EACd,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,OAAO,EACP,QAAQ,EACR,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC;AA8CpB,eAAO,MAAM,EAAE;IACb;;;;OAIG;yBACkB,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,cAAc;IAYnE;;;;;OAKG;kBAEM,MAAM,GACZ,SAAS,CAAC,OAAO,GAAG;QAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAAA;KAAE,CAAC,GAAG,cAAc;IA4DtE;;;;OAIG;iBACU,MAAM,aAAa,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC;IAU7D;;;;;;;;;OASG;uBAEI,MAAM,GACV,SAAS,CAAC,mBAAmB,CAAC,GAAG,cAAc;IA0JlD;;;OAGG;;QAED;;;;WAIG;6BACkB,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,cAAc;QAYnE;;;WAGG;sBAEM,MAAM,GAEX,SAAS,CAAC,OAAO,GAAG;YAAE,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;SAAE,CAAC,GAC/D,cAAc;QAwClB;;;;WAIG;qBACU,MAAM,aAAa,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC;;CAehE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,332 @@
1
+ import { v3 as v3Internal, provinceMerges, wardMappings, } from './internal/v3/index.js';
2
+ import { legacy as legacyInternal } from './internal/legacy/index.js';
3
+ import { normalizeProvince, normalizeCommune, normalizeDistrict, normalizeName, ok, fail, } from './internal/helpers.js';
4
+ import { parseAddress, fuzzySearchProvinces } from './internal/address.js';
5
+ // ─── Private helpers ────────────────────────────────────
6
+ function resolveOldProvinceId(oldId) {
7
+ const trimmed = oldId.trim();
8
+ for (const merge of provinceMerges) {
9
+ const match = merge.mergedFrom.find((m) => m.oldProvinceId === trimmed);
10
+ if (match) {
11
+ const current = v3Internal.getProvinceById(merge.newProvinceId);
12
+ if (current) {
13
+ return { ...normalizeProvince(current), kind: 'province' };
14
+ }
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ function resolveOldCommuneId(oldId) {
20
+ const trimmed = oldId.trim();
21
+ const rawMappings = wardMappings;
22
+ for (const mapping of rawMappings) {
23
+ if (mapping.oldWardCode === trimmed) {
24
+ const c = v3Internal.getCommuneById(mapping.newWardCode);
25
+ if (c) {
26
+ return { ...normalizeCommune(c), kind: 'ward' };
27
+ }
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ // ─── Public API ─────────────────────────────────────────
33
+ export const vn = {
34
+ /**
35
+ * Get administrative divisions.
36
+ * - No arguments → 34 provinces (v3 system)
37
+ * - With parentId → communes/wards under that province (v3 system)
38
+ */
39
+ divisions(parentId) {
40
+ if (parentId === undefined) {
41
+ const list = v3Internal.provinces();
42
+ if (list.length === 0)
43
+ return fail('empty_province_list');
44
+ return ok(list.map(normalizeProvince));
45
+ }
46
+ if (parentId.length === 0)
47
+ return fail('invalid_province_id');
48
+ const list = v3Internal.getCommunesByProvinceId(parentId);
49
+ if (list.length === 0)
50
+ return fail('empty_commune_list');
51
+ return ok(list.map(normalizeCommune));
52
+ },
53
+ /**
54
+ * Look up a single unit by ID, code, or name.
55
+ * Auto-detects: province ID (2-char like "79"), commune ID (5-char like "00004"),
56
+ * province code ("HCM", "HNI"), or full name.
57
+ * Auto-resolves: old province IDs via merge/mapping data.
58
+ */
59
+ lookup(query) {
60
+ if (!query || query.length === 0)
61
+ return fail('invalid_query');
62
+ const trimmed = query.trim();
63
+ // 1. Try as province ID (2 chars)
64
+ if (trimmed.length === 2 && /^\d+$/.test(trimmed)) {
65
+ const p = v3Internal.getProvinceById(trimmed);
66
+ if (p) {
67
+ return ok({ ...normalizeProvince(p), kind: 'province' });
68
+ }
69
+ // Try auto-resolve from old province ID via merge data
70
+ const resolved = resolveOldProvinceId(trimmed);
71
+ if (resolved) {
72
+ return ok(resolved);
73
+ }
74
+ }
75
+ // 1.5 Try as old commune ID via ward mappings
76
+ if (trimmed.length === 5) {
77
+ const resolvedCommune = resolveOldCommuneId(trimmed);
78
+ if (resolvedCommune) {
79
+ return ok(resolvedCommune);
80
+ }
81
+ }
82
+ // 2. Try as province code (3 uppercase letters)
83
+ if (trimmed.length === 3 && /^[A-Za-z]+$/.test(trimmed)) {
84
+ const p = v3Internal.getProvinceByCode(trimmed.toUpperCase());
85
+ if (p) {
86
+ return ok({ ...normalizeProvince(p), kind: 'province' });
87
+ }
88
+ }
89
+ // 3. Try as commune ID (5 chars)
90
+ if (trimmed.length === 5) {
91
+ const c = v3Internal.getCommuneById(trimmed);
92
+ if (c) {
93
+ return ok({ ...normalizeCommune(c), kind: 'ward' });
94
+ }
95
+ }
96
+ // 4. Try as full province name (exact match, case-insensitive)
97
+ const provinceByName = v3Internal.searchProvinceByName(trimmed);
98
+ if (provinceByName.length === 1) {
99
+ return ok({
100
+ ...normalizeProvince(provinceByName[0]),
101
+ kind: 'province',
102
+ });
103
+ }
104
+ // 5. Try auto-resolve old province/commune ID
105
+ const resolvedProv = resolveOldProvinceId(trimmed);
106
+ if (resolvedProv)
107
+ return ok(resolvedProv);
108
+ const resolvedCom = resolveOldCommuneId(trimmed);
109
+ if (resolvedCom)
110
+ return ok(resolvedCom);
111
+ return fail('province_not_found');
112
+ },
113
+ /**
114
+ * Search divisions by name (case-insensitive, partial match).
115
+ * - Without parentId → search in provinces
116
+ * - With parentId → search in communes of that province
117
+ */
118
+ search(name, parentId) {
119
+ if (!name)
120
+ return ok([]);
121
+ if (parentId) {
122
+ return ok(v3Internal.searchCommunesByName(parentId, name).map(normalizeCommune));
123
+ }
124
+ return ok(v3Internal.searchProvinceByName(name).map(normalizeProvince));
125
+ },
126
+ /**
127
+ * Look up a full address string in both legacy and v3 systems.
128
+ *
129
+ * Parses a comma-separated address (e.g. "Xã Đông Hòa, TP Thái Bình, Thái Bình"
130
+ * or "Dong Hoa, Thai Binh"), resolves province/district/commune in the legacy
131
+ * system (63 tỉnh), then cross-references to the v3 system (34 tỉnh).
132
+ *
133
+ * **Strict mode (Option B):** Must find exactly one province — fails with
134
+ * `address_missing_province` or `address_ambiguous_province` otherwise.
135
+ */
136
+ addressLookup(raw) {
137
+ if (!raw || raw.trim().length === 0)
138
+ return fail('invalid_query');
139
+ // 1. Parse address into parts
140
+ const parsed = parseAddress(raw);
141
+ if (!parsed.province) {
142
+ return fail('address_missing_province');
143
+ }
144
+ // 2. Build searchable lists
145
+ const legacyProvinceList = legacyInternal
146
+ .provinces()
147
+ .map(normalizeProvince);
148
+ const v3ProvinceList = v3Internal.provinces().map(normalizeProvince);
149
+ const nProvinceQuery = normalizeName(parsed.province);
150
+ // 3. Find province in legacy
151
+ const legacyProvinceMatches = fuzzySearchProvinces(parsed.province, legacyProvinceList);
152
+ if (legacyProvinceMatches.length === 0) {
153
+ return fail('address_ambiguous_province');
154
+ }
155
+ if (legacyProvinceMatches.length > 1) {
156
+ return fail('address_ambiguous_province');
157
+ }
158
+ const legacyProvince = legacyProvinceMatches[0];
159
+ // 4. Find province in v3
160
+ let v3Province = null;
161
+ const v3ProvinceMatches = fuzzySearchProvinces(parsed.province, v3ProvinceList);
162
+ if (v3ProvinceMatches.length === 1) {
163
+ v3Province = v3ProvinceMatches[0];
164
+ }
165
+ else if (v3ProvinceMatches.length > 1) {
166
+ return fail('address_ambiguous_province');
167
+ }
168
+ // 4b. Cross-resolve: legacy → v3 via provinceMerges
169
+ if (!v3Province) {
170
+ for (const merge of provinceMerges) {
171
+ const found = merge.mergedFrom.find((m) => m.oldProvinceId === legacyProvince.id);
172
+ if (found) {
173
+ const resolved = v3Internal.getProvinceById(merge.newProvinceId);
174
+ if (resolved) {
175
+ v3Province = normalizeProvince(resolved);
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ // 5. Resolve district + commune in legacy
182
+ let legacyDistrict = null;
183
+ let legacyCommune = null;
184
+ const legacyDistricts = legacyInternal.getDistrictsByProvinceId(legacyProvince.id);
185
+ if (parsed.district) {
186
+ const districtMatches = fuzzySearchProvinces(parsed.district, legacyDistricts.map(normalizeDistrict));
187
+ if (districtMatches.length > 0) {
188
+ legacyDistrict = districtMatches[0];
189
+ }
190
+ }
191
+ if (parsed.commune) {
192
+ // Search within district first (if found), then within whole province
193
+ if (legacyDistrict) {
194
+ const communeMatches = fuzzySearchProvinces(parsed.commune, legacyInternal
195
+ .getCommunesByDistrictId(legacyDistrict.id)
196
+ .map(normalizeCommune));
197
+ if (communeMatches.length > 0) {
198
+ legacyCommune = communeMatches[0];
199
+ }
200
+ }
201
+ if (!legacyCommune) {
202
+ const communeMatches = fuzzySearchProvinces(parsed.commune, legacyInternal
203
+ .getCommunesByProvinceId(legacyProvince.id)
204
+ .map(normalizeCommune));
205
+ if (communeMatches.length > 0) {
206
+ legacyCommune = communeMatches[0];
207
+ }
208
+ }
209
+ }
210
+ // 6. Resolve commune in v3
211
+ let v3Commune = null;
212
+ if (v3Province) {
213
+ // 6a. Try ward_mappings if we have a legacy commune
214
+ if (legacyCommune) {
215
+ const rawMappings = wardMappings;
216
+ const match = rawMappings.find((m) => m.oldWardCode === legacyCommune.id);
217
+ if (match) {
218
+ const resolved = v3Internal.getCommuneById(match.newWardCode);
219
+ if (resolved) {
220
+ v3Commune = normalizeCommune(resolved);
221
+ }
222
+ }
223
+ }
224
+ // 6b. Fall back to searching by name in v3 province communes
225
+ if (!v3Commune && parsed.commune) {
226
+ const v3Communes = v3Internal
227
+ .getCommunesByProvinceId(v3Province.id)
228
+ .map(normalizeCommune);
229
+ const communeMatches = fuzzySearchProvinces(parsed.commune, v3Communes);
230
+ if (communeMatches.length > 0) {
231
+ v3Commune = communeMatches[0];
232
+ }
233
+ }
234
+ }
235
+ // 7. Build result
236
+ const result = {
237
+ legacy: {
238
+ province: legacyProvince,
239
+ district: legacyDistrict,
240
+ commune: legacyCommune,
241
+ hamlet: parsed.hamlet,
242
+ },
243
+ v3: {
244
+ province: v3Province,
245
+ commune: v3Commune,
246
+ hamlet: parsed.hamlet,
247
+ },
248
+ };
249
+ return ok(result);
250
+ },
251
+ /**
252
+ * Legacy system (63 provinces, 3 levels: province → district → commune).
253
+ * Explicit opt-in for backward compatibility.
254
+ */
255
+ legacy: {
256
+ /**
257
+ * Get legacy divisions.
258
+ * - No arguments → 63 provinces
259
+ * - With parentId → districts under that province
260
+ */
261
+ divisions(parentId) {
262
+ if (parentId === undefined) {
263
+ const list = legacyInternal.provinces();
264
+ if (list.length === 0)
265
+ return fail('empty_province_list');
266
+ return ok(list.map(normalizeProvince));
267
+ }
268
+ if (parentId.length === 0)
269
+ return fail('invalid_province_id');
270
+ const list = legacyInternal.getDistrictsByProvinceId(parentId);
271
+ if (list.length === 0)
272
+ return fail('empty_district_list');
273
+ return ok(list.map(normalizeDistrict));
274
+ },
275
+ /**
276
+ * Look up a legacy unit by ID.
277
+ * Auto-detects: province ID (2-char), district ID (3-char), commune ID (5-char).
278
+ */
279
+ lookup(query) {
280
+ if (!query || query.length === 0)
281
+ return fail('invalid_query');
282
+ const trimmed = query.trim();
283
+ // Try province ID (2 chars)
284
+ if (trimmed.length === 2) {
285
+ const p = legacyInternal.getProvinceById(trimmed);
286
+ if (p) {
287
+ return ok({ ...normalizeProvince(p), kind: 'province' });
288
+ }
289
+ }
290
+ // Try district ID (3 chars)
291
+ if (trimmed.length === 3) {
292
+ const d = legacyInternal.getDistrictById(trimmed);
293
+ if (d) {
294
+ return ok({ ...normalizeDistrict(d), kind: 'district' });
295
+ }
296
+ }
297
+ // Try commune ID (5 chars)
298
+ if (trimmed.length === 5) {
299
+ const c = legacyInternal.getCommuneById(trimmed);
300
+ if (c) {
301
+ return ok({ ...normalizeCommune(c), kind: 'ward' });
302
+ }
303
+ }
304
+ // Try province by name
305
+ const byName = legacyInternal.searchByName(trimmed);
306
+ if (byName.length === 1) {
307
+ return ok({
308
+ ...normalizeProvince(byName[0]),
309
+ kind: 'province',
310
+ });
311
+ }
312
+ return fail('province_not_found');
313
+ },
314
+ /**
315
+ * Search legacy divisions by name.
316
+ * - Without parentId → search in provinces
317
+ * - With parentId → search in districts of that province
318
+ */
319
+ search(name, parentId) {
320
+ if (!name)
321
+ return ok([]);
322
+ if (parentId) {
323
+ return ok(legacyInternal
324
+ .getDistrictsByProvinceId(parentId)
325
+ .filter((d) => d.name.toLowerCase().includes(name.toLowerCase().trim()))
326
+ .map(normalizeDistrict));
327
+ }
328
+ return ok(legacyInternal.searchByName(name).map(normalizeProvince));
329
+ },
330
+ },
331
+ };
332
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { GeoUnit } from '../types.js';
2
+ /**
3
+ * Detect level of an address part by its prefix.
4
+ * @param provinceFound — if true, treat ambiguous "Thành phố/TP" as district.
5
+ */
6
+ export declare function detectLevel(part: string, provinceFound?: boolean): 'province' | 'district' | 'commune' | 'hamlet' | 'unknown';
7
+ export interface ParsedAddress {
8
+ /** Deduplicated, trimmed, original-case parts (right-to-left order). */
9
+ parts: string[];
10
+ hamlet: string | null;
11
+ commune: string | null;
12
+ district: string | null;
13
+ province: string | null;
14
+ }
15
+ /**
16
+ * Split, trim, dedup (by normalized form), then classify each part.
17
+ * Returns structured parts without doing any DB lookup.
18
+ */
19
+ export declare function parseAddress(raw: string): ParsedAddress;
20
+ /**
21
+ * Expand common Vietnamese administrative abbreviations.
22
+ * "TP HCM" → "thành phố hồ chí minh", "TX" → "thị xã", etc.
23
+ */
24
+ export declare function expandAbbreviations(name: string): string;
25
+ /**
26
+ * Check if a query matches a target name.
27
+ * - Exact normalized match
28
+ * - Target contains query (token-level)
29
+ * - Query contains target
30
+ */
31
+ export declare function fuzzyMatch(query: string, target: string): boolean;
32
+ /**
33
+ * Find best matches in a list of GeoUnits.
34
+ * Returns exact matches first, then partial matches.
35
+ */
36
+ export declare function fuzzySearchProvinces(query: string, items: GeoUnit[]): GeoUnit[];
37
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/internal/address.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAU3C;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,aAAa,CAAC,EAAE,OAAO,GACtB,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CA4B5D;AAED,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CA8DvD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOxD;AASD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQjE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EAAE,GACf,OAAO,EAAE,CAkBX"}
@@ -0,0 +1,164 @@
1
+ import { normalizeName } from './helpers.js';
2
+ /** Level prefixes ordered from most specific to least. */
3
+ const HAMLET_PREFIXES = ['thôn', 'ấp', 'tổ', 'xóm', 'bản', 'số'];
4
+ const COMMUNE_PREFIXES = ['xã', 'phường', 'thị trấn', 'tt.', 'tt'];
5
+ const DISTRICT_PREFIXES = ['huyện', 'quận', 'thị xã', 'tx.', 'tx'];
6
+ /** "Thành phố" and "tp" are ambiguous — could be province or district. */
7
+ const AMBIGUOUS_PREFIXES = ['thành phố', 'tp.', 'tp'];
8
+ const PROVINCE_PREFIXES = ['tỉnh'];
9
+ /**
10
+ * Detect level of an address part by its prefix.
11
+ * @param provinceFound — if true, treat ambiguous "Thành phố/TP" as district.
12
+ */
13
+ export function detectLevel(part, provinceFound) {
14
+ const lower = part.toLowerCase().trim();
15
+ // Exclusive province prefix
16
+ for (const prefix of PROVINCE_PREFIXES) {
17
+ if (lower.startsWith(prefix) || lower.endsWith(prefix)) {
18
+ return 'province';
19
+ }
20
+ }
21
+ // Ambiguous: if province already found, treat as district
22
+ for (const prefix of AMBIGUOUS_PREFIXES) {
23
+ if (lower.startsWith(prefix) || lower.endsWith(prefix)) {
24
+ return provinceFound ? 'district' : 'province';
25
+ }
26
+ }
27
+ for (const prefix of DISTRICT_PREFIXES) {
28
+ if (lower.startsWith(prefix))
29
+ return 'district';
30
+ }
31
+ for (const prefix of COMMUNE_PREFIXES) {
32
+ if (lower.startsWith(prefix))
33
+ return 'commune';
34
+ }
35
+ for (const prefix of HAMLET_PREFIXES) {
36
+ if (lower.startsWith(prefix))
37
+ return 'hamlet';
38
+ }
39
+ return 'unknown';
40
+ }
41
+ /**
42
+ * Split, trim, dedup (by normalized form), then classify each part.
43
+ * Returns structured parts without doing any DB lookup.
44
+ */
45
+ export function parseAddress(raw) {
46
+ if (!raw) {
47
+ return { parts: [], hamlet: null, commune: null, district: null, province: null };
48
+ }
49
+ // Split by comma, trim, filter empty
50
+ const rawParts = raw.split(',').map((s) => s.trim()).filter(Boolean);
51
+ if (rawParts.length === 0) {
52
+ return { parts: [], hamlet: null, commune: null, district: null, province: null };
53
+ }
54
+ // Dedup by normalized form (keep first occurrence)
55
+ const seen = new Set();
56
+ const deduped = [];
57
+ for (const p of rawParts) {
58
+ const norm = normalizeName(p);
59
+ if (norm && !seen.has(norm)) {
60
+ seen.add(norm);
61
+ deduped.push(p);
62
+ }
63
+ }
64
+ // Classify right-to-left
65
+ let hamlet = null;
66
+ let commune = null;
67
+ let district = null;
68
+ let province = null;
69
+ // Reverse order: rightmost = province (broadest scope in VN address format)
70
+ const reversed = [...deduped].reverse();
71
+ // Phase 1: rightmost part is ALWAYS the province
72
+ province = reversed[0];
73
+ // Phase 2: classify remaining parts (left of province)
74
+ const remaining = deduped.slice(0, -1); // all except the last (rightmost)
75
+ for (const part of remaining) {
76
+ const level = detectLevel(part, true); // province already found
77
+ if (level === 'hamlet' && !hamlet) {
78
+ hamlet = part;
79
+ }
80
+ else if (level === 'commune' && !commune) {
81
+ commune = part;
82
+ }
83
+ else if (level === 'district' && !district) {
84
+ district = part;
85
+ }
86
+ else if (!commune) {
87
+ // Unknown → treat as commune (most specific known level)
88
+ commune = part;
89
+ }
90
+ else if (!district) {
91
+ district = part;
92
+ }
93
+ else if (!hamlet) {
94
+ hamlet = part;
95
+ }
96
+ }
97
+ return {
98
+ parts: deduped,
99
+ hamlet,
100
+ commune,
101
+ district,
102
+ province,
103
+ };
104
+ }
105
+ /**
106
+ * Expand common Vietnamese administrative abbreviations.
107
+ * "TP HCM" → "thành phố hồ chí minh", "TX" → "thị xã", etc.
108
+ */
109
+ export function expandAbbreviations(name) {
110
+ return name
111
+ .replace(/\btp\.?\b/gi, 'thành phố')
112
+ .replace(/\btx\.?\b/gi, 'thị xã')
113
+ .replace(/\btt\.?\b/gi, 'thị trấn')
114
+ .replace(/\bhcm\b/gi, 'hồ chí minh')
115
+ .replace(/\bhni\b/gi, 'hà nội');
116
+ }
117
+ /**
118
+ * Normalize for search: expand abbreviations first, THEN remove diacritics.
119
+ */
120
+ function normalizeForSearch(name) {
121
+ return normalizeName(expandAbbreviations(name));
122
+ }
123
+ /**
124
+ * Check if a query matches a target name.
125
+ * - Exact normalized match
126
+ * - Target contains query (token-level)
127
+ * - Query contains target
128
+ */
129
+ export function fuzzyMatch(query, target) {
130
+ const nq = normalizeForSearch(query);
131
+ const nt = normalizeForSearch(target);
132
+ if (!nq || !nt)
133
+ return false;
134
+ if (nq === nt)
135
+ return true;
136
+ if (nt.includes(nq))
137
+ return true;
138
+ if (nq.includes(nt))
139
+ return true;
140
+ return false;
141
+ }
142
+ /**
143
+ * Find best matches in a list of GeoUnits.
144
+ * Returns exact matches first, then partial matches.
145
+ */
146
+ export function fuzzySearchProvinces(query, items) {
147
+ const nq = normalizeForSearch(query);
148
+ if (!nq)
149
+ return [];
150
+ const exact = [];
151
+ const partial = [];
152
+ for (const item of items) {
153
+ const ni = normalizeForSearch(item.name);
154
+ if (ni === nq) {
155
+ exact.push(item);
156
+ }
157
+ else if (ni.includes(nq)) {
158
+ partial.push(item);
159
+ }
160
+ }
161
+ // Exact matches first
162
+ return [...exact, ...partial];
163
+ }
164
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../../src/internal/address.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { GeoUnit, GeoError } from '../types.js';
2
+ /** Remove Vietnamese diacritics, lowercase, collapse whitespace. */
3
+ export declare function normalizeName(name: string): string;
4
+ export declare function normalizeProvince(p: {
5
+ idProvince: string;
6
+ name: string;
7
+ }): GeoUnit;
8
+ export declare function normalizeCommune(c: {
9
+ idCommune: string;
10
+ name: string;
11
+ }): GeoUnit;
12
+ export declare function normalizeDistrict(d: {
13
+ idDistrict: string;
14
+ name: string;
15
+ }): GeoUnit;
16
+ export declare function ok<T>(data: T): {
17
+ data: T;
18
+ error?: undefined;
19
+ };
20
+ export declare function fail(error: GeoError): {
21
+ data: null;
22
+ error: GeoError;
23
+ };
24
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/internal/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAgCrD,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAEV;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAEV;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAEV;AAED,wBAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,CAE7D;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAErE"}