@mapslibvn/core 0.4.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.
package/dist/index.js ADDED
@@ -0,0 +1,830 @@
1
+ // src/attribution.ts
2
+ var ATTRIBUTION_LINKS = [
3
+ {
4
+ text: "\xA9 MapsLibVN",
5
+ href: "https://github.com/dotienphong/maps-library-vietnam"
6
+ },
7
+ {
8
+ text: "\xA9 OpenStreetMap contributors",
9
+ href: "https://www.openstreetmap.org/copyright",
10
+ license: "ODbL"
11
+ },
12
+ { text: "\xA9 OpenMapTiles", href: "https://openmaptiles.org/" },
13
+ {
14
+ text: "Places: Overture Maps Foundation",
15
+ href: "https://overturemaps.org/",
16
+ license: "CDLA-Permissive 2.0"
17
+ },
18
+ {
19
+ text: "Foursquare OS Places",
20
+ href: "https://opensource.foursquare.com/os-places/",
21
+ license: "Apache-2.0"
22
+ }
23
+ ];
24
+ var withLicense = (link, text) => link.license ? `${text} (${link.license})` : text;
25
+ function escapeHtml(value) {
26
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
27
+ }
28
+ function join(parts) {
29
+ const [mapsLibVN, osm, openMapTiles, overture, foursquare] = parts;
30
+ return `${mapsLibVN} \xB7 ${osm} \xB7 ${openMapTiles} \xB7 ${overture}, ${foursquare}`;
31
+ }
32
+ function attributionText() {
33
+ return join(ATTRIBUTION_LINKS.map((link) => withLicense(link, link.text)));
34
+ }
35
+ function attributionHtml() {
36
+ return join(
37
+ ATTRIBUTION_LINKS.map(
38
+ (link) => withLicense(
39
+ link,
40
+ `<a href="${link.href}" target="_blank" rel="noopener">${escapeHtml(link.text)}</a>`
41
+ )
42
+ )
43
+ );
44
+ }
45
+
46
+ // src/errors.ts
47
+ var MapsLibVNError = class extends Error {
48
+ status;
49
+ code;
50
+ requestId;
51
+ constructor(status, code, message, requestId) {
52
+ super(message);
53
+ this.name = "MapsLibVNError";
54
+ this.status = status;
55
+ this.code = code;
56
+ this.requestId = requestId;
57
+ }
58
+ };
59
+
60
+ // src/poi-sources.ts
61
+ var POI_SOURCES = ["osm", "overture", "fsq"];
62
+ var POI_SOURCE_PROFILES = {
63
+ all: ["osm", "overture", "fsq"],
64
+ osm: ["osm"],
65
+ "osm-fsq": ["osm", "fsq"],
66
+ "overture-fsq": ["overture", "fsq"],
67
+ overture: ["overture"],
68
+ fsq: ["fsq"]
69
+ };
70
+ var DEFAULT_POI_SOURCES = POI_SOURCE_PROFILES.all;
71
+ var isPoiSource = (value) => POI_SOURCES.includes(value);
72
+ function normalizePoiSources(list) {
73
+ if (list.length === 0 || !list.every(isPoiSource)) return null;
74
+ const set = new Set(list);
75
+ return POI_SOURCES.filter((source) => set.has(source));
76
+ }
77
+ function parsePoiSourcesCsv(raw) {
78
+ const trimmed = raw?.trim() ?? "";
79
+ if (!trimmed) return [...DEFAULT_POI_SOURCES];
80
+ if (trimmed === "all") return [...POI_SOURCE_PROFILES.all];
81
+ return normalizePoiSources(trimmed.split(",").map((part) => part.trim()));
82
+ }
83
+ function poiSourcesKey(sources) {
84
+ return (normalizePoiSources(sources) ?? []).join(",");
85
+ }
86
+ function profileForSources(sources) {
87
+ const key = poiSourcesKey(sources);
88
+ for (const [profile, list] of Object.entries(POI_SOURCE_PROFILES)) {
89
+ if (list.join(",") === key) return profile;
90
+ }
91
+ return null;
92
+ }
93
+ function poiSourceClause(arrayExpr) {
94
+ return `(p.primary_source = ANY(${arrayExpr}) OR p.created_by = 'user')`;
95
+ }
96
+
97
+ // src/client.ts
98
+ function createClient(options) {
99
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
100
+ const poiSources = normalizePoiSources(options.poiSources ?? DEFAULT_POI_SOURCES);
101
+ if (!poiSources) {
102
+ throw new Error(`poiSources kh\xF4ng h\u1EE3p l\u1EC7: ${JSON.stringify(options.poiSources)}`);
103
+ }
104
+ const sources = poiSourcesKey(poiSources);
105
+ const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
106
+ const baseHeaders = () => ({
107
+ ...options.headers ?? {},
108
+ "X-Api-Key": options.apiKey
109
+ });
110
+ async function parseOrThrow(response) {
111
+ if (!response.ok) {
112
+ let body = {};
113
+ try {
114
+ body = await response.json();
115
+ } catch {
116
+ }
117
+ throw new MapsLibVNError(
118
+ response.status,
119
+ body.error?.code ?? "http_error",
120
+ body.error?.message ?? `HTTP ${response.status}`,
121
+ body.error?.request_id
122
+ );
123
+ }
124
+ return await response.json();
125
+ }
126
+ async function get(path, params = {}) {
127
+ const url = new URL(baseUrl + path);
128
+ for (const [key, value] of Object.entries(params)) {
129
+ if (value !== void 0) url.searchParams.set(key, String(value));
130
+ }
131
+ const response = await doFetch(url, {
132
+ headers: baseHeaders()
133
+ });
134
+ return parseOrThrow(response);
135
+ }
136
+ async function post(path, body) {
137
+ const response = await doFetch(new URL(baseUrl + path), {
138
+ method: "POST",
139
+ headers: { ...baseHeaders(), "content-type": "application/json" },
140
+ body: JSON.stringify(body)
141
+ });
142
+ return parseOrThrow(response);
143
+ }
144
+ return {
145
+ baseUrl,
146
+ attribution: () => get("/v1/attribution"),
147
+ styleUrl: (theme) => `${baseUrl}/v1/styles/${theme}.json?key=${encodeURIComponent(options.apiKey)}&sources=${encodeURIComponent(sources)}`,
148
+ autocomplete: (q, opts = {}) => get("/v1/autocomplete", {
149
+ q,
150
+ near: opts.near?.join(","),
151
+ limit: opts.limit,
152
+ types: opts.types?.join(","),
153
+ sources
154
+ }),
155
+ search: (q, opts = {}) => get("/v1/search", {
156
+ q,
157
+ category: opts.category,
158
+ near: opts.near?.join(","),
159
+ radius: opts.radius,
160
+ bbox: opts.bbox?.join(","),
161
+ limit: opts.limit,
162
+ offset: opts.offset,
163
+ sources
164
+ }),
165
+ nearby: (opts) => get("/v1/nearby", {
166
+ lat: opts.lat,
167
+ lng: opts.lng,
168
+ radius: opts.radius,
169
+ category: opts.category,
170
+ limit: opts.limit,
171
+ sources
172
+ }),
173
+ getPlace: (id) => get(`/v1/places/${encodeURIComponent(id)}`),
174
+ geocode: (q, opts = {}) => get("/v1/geocode", {
175
+ q,
176
+ near: opts.near?.join(","),
177
+ limit: opts.limit
178
+ }),
179
+ reverse: (lat, lng) => get("/v1/reverse", { lat, lng, sources }),
180
+ /** Gửi đóng góp/sửa POI (spec 6.1). Khoá phải có scope edits:write. */
181
+ suggestEdit: (edit) => post("/v1/edits", edit)
182
+ };
183
+ }
184
+
185
+ // src/abbrev.json
186
+ var abbrev_default = {
187
+ "tp.": "thanh pho",
188
+ "tt.": "thi tran",
189
+ "tx.": "thi xa",
190
+ "p.": "phuong",
191
+ "q.": "quan",
192
+ "h.": "huyen",
193
+ "x.": "xa",
194
+ "d.": "duong",
195
+ "ng.": "nguyen",
196
+ kp: "khu pho",
197
+ cty: "cong ty",
198
+ cp: "co phan"
199
+ };
200
+
201
+ // src/brand_alias.json
202
+ var brand_alias_default = {
203
+ highlands: ["highlands coffee", "highland coffee", "highland"],
204
+ cong: ["cong caphe", "cong ca phe", "cong cafe", "cong coffee"],
205
+ "the coffee house": ["tch", "coffee house"],
206
+ "phuc long": ["phuc long coffee tea", "phuc long tea coffee", "phuc long coffee"],
207
+ "trung nguyen": ["trung nguyen legend", "trung nguyen e-coffee", "trung nguyen coffee"],
208
+ starbucks: ["starbucks coffee", "starbucks reserve"],
209
+ "circle k": ["circlek"],
210
+ winmart: ["vinmart", "vinmart plus", "winmart plus"],
211
+ coopmart: ["co opmart", "co op mart", "coop mart", "saigon co op"],
212
+ "bach hoa xanh": ["bhx"],
213
+ "family mart": ["familymart"],
214
+ gs25: ["gs 25"],
215
+ "pho 24": ["pho24"],
216
+ kfc: ["kentucky fried chicken"],
217
+ vietcombank: ["ngan hang vietcombank", "ngan hang tmcp ngoai thuong viet nam", "vcb"],
218
+ techcombank: ["ngan hang techcombank", "tcb"],
219
+ agribank: ["ngan hang agribank", "ngan hang nong nghiep va phat trien nong thon"],
220
+ bidv: ["ngan hang bidv", "ngan hang dau tu va phat trien viet nam"],
221
+ vietinbank: ["ngan hang vietinbank", "ngan hang cong thuong viet nam"],
222
+ pharmacity: ["nha thuoc pharmacity"],
223
+ "long chau": ["nha thuoc long chau", "fpt long chau"],
224
+ "an khang": ["nha thuoc an khang"],
225
+ "the gioi di dong": ["tgdd"],
226
+ "dien may xanh": ["dmx"],
227
+ "fpt shop": ["fptshop"]
228
+ };
229
+
230
+ // src/normalize.ts
231
+ var ABBREV = abbrev_default;
232
+ var BRAND_ALIAS = brand_alias_default;
233
+ var NAME_FILLERS = [
234
+ "cong ty",
235
+ "cty",
236
+ "tnhh",
237
+ "mtv",
238
+ "co phan",
239
+ "cua hang",
240
+ "quan",
241
+ "tiem",
242
+ "nha hang",
243
+ "shop",
244
+ "cafe",
245
+ "ca phe",
246
+ "coffee"
247
+ ];
248
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
249
+ var ABBREV_RULES = Object.entries(ABBREV).map(([key, value]) => {
250
+ const after = key.endsWith(".") ? "" : "(?=[\\s\\d,.]|$)";
251
+ return {
252
+ re: new RegExp(`(^|[\\s,.(])${escapeRegExp(key)}${after}`, "g"),
253
+ replacement: `$1${value} `
254
+ };
255
+ });
256
+ var ALIAS_RULES = Object.entries(BRAND_ALIAS).flatMap(([canonical, variants]) => variants.map((variant) => ({ variant, canonical }))).sort((a, b) => b.variant.length - a.variant.length);
257
+ function stripDiacritics(s) {
258
+ return s.normalize("NFD").replace(/\p{M}/gu, "").replace(/đ/g, "d").replace(/Đ/g, "D");
259
+ }
260
+ function expandAbbrev(s) {
261
+ let out = s;
262
+ for (const { re, replacement } of ABBREV_RULES) out = out.replace(re, replacement);
263
+ out = out.replace(/(^|\s)f\.?(?=\s*\d)/g, "$1phuong ");
264
+ return out.replace(/ {2,}/g, " ").trim();
265
+ }
266
+ function normalizeVi(input) {
267
+ let s = stripDiacritics(input.normalize("NFC").toLowerCase());
268
+ s = expandAbbrev(s);
269
+ s = s.replace(/[^a-z0-9/\-\s]/g, " ");
270
+ s = s.replace(/\s*\/\s*/g, "/");
271
+ s = s.replace(/\s+-\s+/g, " ");
272
+ return s.replace(/\s+/g, " ").trim();
273
+ }
274
+ function applyBrandAlias(s) {
275
+ for (const { variant, canonical } of ALIAS_RULES) {
276
+ if (s === variant) return canonical;
277
+ if (s.startsWith(`${variant} `)) return canonical + s.slice(variant.length);
278
+ }
279
+ return s;
280
+ }
281
+ function nameCore(input) {
282
+ const norm = normalizeVi(input);
283
+ let s = norm;
284
+ let changed = true;
285
+ while (changed && s) {
286
+ changed = false;
287
+ for (const filler of NAME_FILLERS) {
288
+ if (s === filler) {
289
+ s = "";
290
+ break;
291
+ }
292
+ if (s.startsWith(`${filler} `)) {
293
+ s = s.slice(filler.length + 1);
294
+ changed = true;
295
+ }
296
+ }
297
+ }
298
+ return applyBrandAlias(s || norm);
299
+ }
300
+
301
+ // src/provinces.json
302
+ var provinces_default = {
303
+ "H\xE0 N\u1ED9i": ["ha noi", "hanoi", "hn"],
304
+ Hu\u1EBF: ["hue", "thua thien hue", "thua thien-hue"],
305
+ "Lai Ch\xE2u": ["lai chau"],
306
+ "\u0110i\u1EC7n Bi\xEAn": ["dien bien"],
307
+ "S\u01A1n La": ["son la"],
308
+ "L\u1EA1ng S\u01A1n": ["lang son"],
309
+ "Qu\u1EA3ng Ninh": ["quang ninh", "ha long"],
310
+ "Thanh H\xF3a": ["thanh hoa"],
311
+ "Ngh\u1EC7 An": ["nghe an", "vinh"],
312
+ "H\xE0 T\u0129nh": ["ha tinh"],
313
+ "Cao B\u1EB1ng": ["cao bang"],
314
+ "Tuy\xEAn Quang": ["tuyen quang", "ha giang"],
315
+ "L\xE0o Cai": ["lao cai", "yen bai", "sa pa", "sapa"],
316
+ "Th\xE1i Nguy\xEAn": ["thai nguyen", "bac kan", "bac can"],
317
+ "Ph\xFA Th\u1ECD": ["phu tho", "vinh phuc", "hoa binh", "viet tri"],
318
+ "B\u1EAFc Ninh": ["bac ninh", "bac giang"],
319
+ "H\u01B0ng Y\xEAn": ["hung yen", "thai binh"],
320
+ "H\u1EA3i Ph\xF2ng": ["hai phong", "hai duong"],
321
+ "Ninh B\xECnh": ["ninh binh", "ha nam", "nam dinh"],
322
+ "Qu\u1EA3ng Tr\u1ECB": ["quang tri", "quang binh", "dong hoi"],
323
+ "\u0110\xE0 N\u1EB5ng": ["da nang", "danang", "quang nam", "hoi an", "tam ky"],
324
+ "Qu\u1EA3ng Ng\xE3i": ["quang ngai", "kon tum"],
325
+ "Gia Lai": ["gia lai", "binh dinh", "quy nhon", "pleiku"],
326
+ "Kh\xE1nh H\xF2a": ["khanh hoa", "ninh thuan", "nha trang", "cam ranh", "phan rang"],
327
+ "L\xE2m \u0110\u1ED3ng": ["lam dong", "dak nong", "dac nong", "binh thuan", "da lat", "dalat", "phan thiet"],
328
+ "\u0110\u1EAFk L\u1EAFk": ["dak lak", "dac lac", "daklak", "phu yen", "buon ma thuot", "tuy hoa"],
329
+ "Th\xE0nh ph\u1ED1 H\u1ED3 Ch\xED Minh": ["ho chi minh", "hcm", "tphcm", "sai gon", "saigon", "binh duong", "ba ria vung tau", "ba ria-vung tau", "ba ria", "vung tau", "thu dau mot", "di an"],
330
+ "\u0110\u1ED3ng Nai": ["dong nai", "binh phuoc", "bien hoa", "dong xoai"],
331
+ "T\xE2y Ninh": ["tay ninh", "long an", "tan an"],
332
+ "C\u1EA7n Th\u01A1": ["can tho", "soc trang", "hau giang", "vi thanh"],
333
+ "V\u0129nh Long": ["vinh long", "ben tre", "tra vinh"],
334
+ "\u0110\u1ED3ng Th\xE1p": ["dong thap", "tien giang", "my tho", "cao lanh", "sa dec"],
335
+ "C\xE0 Mau": ["ca mau", "bac lieu"],
336
+ "An Giang": ["an giang", "kien giang", "rach gia", "phu quoc", "long xuyen", "chau doc"]
337
+ };
338
+
339
+ // src/address.ts
340
+ var PROVINCE_BY_ALIAS = /* @__PURE__ */ new Map();
341
+ for (const [name, aliases] of Object.entries(provinces_default)) {
342
+ PROVINCE_BY_ALIAS.set(normalizeVi(name).replace(/^thanh pho /, ""), name);
343
+ for (const a of aliases) PROVINCE_BY_ALIAS.set(a, name);
344
+ }
345
+ var NUM = String.raw`(?:\d+(?:[a-z](?![a-z])\d*)?(?:-\d+[a-z]?)?|[a-z]{1,2}\d+[a-z]?)`;
346
+ var NUM_LEAD = String.raw`(?:\d+(?:[a-z](?![a-z])\d*)?(?:-\d+[a-z]?)?|(?![pqf]\d|tp\d|ql\d|tl\d|dt\d|hl\d)[a-z]{1,2}\d+[a-z]?)`;
347
+ var HN = String.raw`${NUM_LEAD}(?:/${NUM})*`;
348
+ var ALLEY_KW = String.raw`(?:hem|ngo|ngach|kiet)`;
349
+ var PREFIX = String.raw`(?:(?:so(?:\s*nha)?|sn|lo|can|kiot)\.?\s*)?`;
350
+ var RE_PREFIX = new RegExp(`^${PREFIX}`);
351
+ var RE_STREET = new RegExp(
352
+ String.raw`^${PREFIX}(${HN})?\s*((?:${ALLEY_KW}\s*${HN}\s*)*)((?:duong|d\.|pho)\s+(?!so\b|[a-z]{1,2}\d))?(.*)$`
353
+ );
354
+ var RE_ALLEY_EACH = new RegExp(String.raw`(${ALLEY_KW})\s*(${HN})`, "g");
355
+ var RE_STREET_LIKE = new RegExp(
356
+ String.raw`^${PREFIX}${NUM_LEAD}(?:/|\s|$)|\b${ALLEY_KW}\s*\d|^(?:duong|d\.|pho)\s`
357
+ );
358
+ var RE_STREET_WORD = /^(?:duong|d\.|pho)\s/;
359
+ var RE_POSITIONAL = /^(?:gan|doi dien|cuoi|canh)\s/;
360
+ var RE_TINH = /^(?:tinh(?!\s+lo\b)|t\.)\s+(.+)$/;
361
+ var RE_CITY = /^(?:thanh pho(?=[\s\d])\s*|tp\.?\s*)(.+)$/;
362
+ var RE_DISTRICT = /^(?:(?:quan|huyen|thi xa)(?=[\s\d])\s*|(?:q\.|h\.|tx\.)\s*)(.+)$|^q\s*(\d.*)$|^district\s+(.+)$/;
363
+ var RE_WARD = /^(?:(?:phuong|xa(?!\s+lo\b)|thi tran)(?=[\s\d])\s*|(?:p\.|x\.|tt\.|f\.)\s*|tt\s+)(.+)$|^[pf]\s*(\d.*)$|^(.+?)\s+ward$/;
364
+ var RE_IGNORE = /^(?:to|khu pho|kp|ap|thon|xom|khom|khu vuc|kv|to dan pho|tdp|lo|toa nha|tn|chung cu|cc|block|tang|lau|can ho|kdc|khu dan cu|kdt|khu do thi|kcn|khu cong nghiep|kcx|khu che xuat|khu cong nghe cao|khu|cu xa|cx|ttm|tttm)\b/;
365
+ var RE_SPLIT = /\s(?=(?:phuong|xa(?!\s+lo\b)|thi tran|quan|huyen|thi xa|thanh pho|tinh(?!\s+lo\b))(?:\s|\d)|(?:p|q|f|tp|tt|tx|h|x)\.\s*\S|tp\s*\S)|(?<!\b(?:lo|can|kiot|block|toa|khu|so))\s(?=[pqf]\s*\d)/g;
366
+ var RE_SPLIT_PART = /\s(?=(?:phuong|thi tran|quan|huyen|thi xa|thanh pho|tinh(?!\s+lo\b))(?:\s|\d)|(?:p|q|f|tp|tt|tx|h)\.\s*\S|tp\s*\S)|(?<!\b(?:lo|can|kiot|block|toa|khu|so))\s(?=[pqf]\s*\d)/g;
367
+ var keyOf = (s) => stripDiacritics(s.toLowerCase());
368
+ var cleanTail = (s) => s.trim().replace(/^[\s,.;:\-/]+|[\s,.;:-]+$/g, "");
369
+ function lookupProvince(part) {
370
+ const n = normalizeVi(part).replace(/^(?:tinh|thanh pho|tp)\s+/, "");
371
+ return PROVINCE_BY_ALIAS.get(n);
372
+ }
373
+ function isStreetWord(orig, at, word) {
374
+ if (word.startsWith("d.")) return true;
375
+ const c0 = orig[at] ?? "";
376
+ if (word.startsWith("duong"))
377
+ return c0 === "\u0110" || c0 === "\u0111" || c0 === "D" || c0 === "d" ? c0 === "\u0110" || c0 === "\u0111" : false;
378
+ const c2 = orig[at + 2] ?? "";
379
+ return c2 === "\u1ED1" || c2 === "\u1ED0";
380
+ }
381
+ function mergeChain(fromAlley, fromHn) {
382
+ const startsWith = (a, b) => b.every((x, i) => a[i] === x);
383
+ if (startsWith(fromHn, fromAlley)) return fromHn;
384
+ if (startsWith(fromAlley, fromHn)) return fromAlley;
385
+ return [...fromAlley, ...fromHn];
386
+ }
387
+ function parseStreetPart(orig, key, out) {
388
+ const m = RE_STREET.exec(key);
389
+ if (!m) return;
390
+ const [, hnRaw, alleyText = "", streetWord = "", rest0 = ""] = m;
391
+ let rest = rest0;
392
+ let restAt = key.length - rest0.length;
393
+ if (streetWord && !isStreetWord(orig, restAt - streetWord.length, streetWord)) {
394
+ rest = streetWord + rest0;
395
+ restAt -= streetWord.length;
396
+ }
397
+ let hn;
398
+ let hnAt = -1;
399
+ if (hnRaw) {
400
+ const prefixLen = RE_PREFIX.exec(key)?.[0].length ?? 0;
401
+ hnAt = key.indexOf(hnRaw, prefixLen);
402
+ hn = orig.slice(hnAt, hnAt + hnRaw.length);
403
+ }
404
+ if (hn && !alleyText && /^thang\s+\d/.test(rest.trim())) {
405
+ out.street = cleanTail(orig.slice(hnAt));
406
+ out.streetNorm = normalizeVi(out.street);
407
+ return;
408
+ }
409
+ const street = cleanTail(
410
+ orig.slice(restAt).replace(/\s(?:[Kk]hóm|[Tt]ổ|[Ấấ]p|KP|[Kk]hu phố)\s+\d.*$/, "")
411
+ );
412
+ if (street) {
413
+ out.street = street;
414
+ out.streetNorm = normalizeVi(street);
415
+ }
416
+ const alleys = [...alleyText.matchAll(RE_ALLEY_EACH)];
417
+ const alleyNums = alleys.map((a) => a[2].toUpperCase().split("/")).reverse().flat();
418
+ const segs = hn ? hn.split("/") : [];
419
+ const house = segs.at(-1);
420
+ const chain = mergeChain(
421
+ alleyNums,
422
+ segs.slice(0, -1).map((s) => s.toUpperCase())
423
+ );
424
+ if (alleys.length) {
425
+ out.alleyKeyword = alleys[0]?.[1];
426
+ out.alleyChain = chain;
427
+ if (house) {
428
+ out.houseInAlley = house;
429
+ out.housenumber = [...chain, house].join("/");
430
+ }
431
+ } else if (hn) {
432
+ out.housenumber = hn;
433
+ out.alleyChain = chain;
434
+ if (chain.length && house) out.houseInAlley = house;
435
+ }
436
+ }
437
+ function parseAddress(input) {
438
+ const out = { alleyChain: [], confidence: 0 };
439
+ const rememberAdmin = (key, value) => {
440
+ out.adminOriginal ??= {};
441
+ if (!out.adminOriginal[key]) out.adminOriginal[key] = value;
442
+ };
443
+ const orig = input.normalize("NFC").replace(/\([^)]*\)?/g, " ").replace(/\s+/g, " ").replace(/\s+[-–—]\s+/g, ", ").trim().replace(/,?\s*(?:việt nam|viet nam|vietnam)\.?\s*$/i, "").replace(/,?\s*\b\d{5,6}\s*$/, "").trim();
444
+ if (!orig) return out;
445
+ const splitBy = (s, re) => {
446
+ const key = keyOf(s);
447
+ const cuts = [...key.matchAll(re)].map((m) => m.index ?? 0);
448
+ return [0, ...cuts].map((start, i, arr) => cleanTail(s.slice(start, arr[i + 1]))).filter(Boolean);
449
+ };
450
+ const commaParts = orig.split(",").map(cleanTail).filter(Boolean);
451
+ const parts = commaParts.length === 1 ? splitBy(orig, RE_SPLIT) : commaParts.flatMap((p) => splitBy(p, RE_SPLIT_PART));
452
+ let streetIdx = -1;
453
+ let firstAdminIdx = Number.POSITIVE_INFINITY;
454
+ const unknown = [];
455
+ const numbers = [];
456
+ parts.forEach((part, i) => {
457
+ const key = keyOf(part);
458
+ if (!key || RE_POSITIONAL.test(key)) return;
459
+ const markAdmin = () => {
460
+ firstAdminIdx = Math.min(firstAdminIdx, i);
461
+ };
462
+ if (streetIdx < 0 && RE_STREET_LIKE.test(key)) {
463
+ parseStreetPart(part, key, out);
464
+ streetIdx = i;
465
+ return;
466
+ }
467
+ if (streetIdx >= 0 && !out.street && i === streetIdx + 1 && RE_STREET_WORD.test(key)) {
468
+ parseStreetPart(part, key, out);
469
+ return;
470
+ }
471
+ const tinh = RE_TINH.exec(key);
472
+ if (tinh) {
473
+ markAdmin();
474
+ rememberAdmin("province", part);
475
+ out.province = lookupProvince(tinh[1] ?? "") ?? cleanTail(part.slice(key.length - (tinh[1] ?? "").length));
476
+ return;
477
+ }
478
+ const city = RE_CITY.exec(key);
479
+ if (city) {
480
+ markAdmin();
481
+ const rest = city[1] ?? "";
482
+ const prov2 = lookupProvince(rest);
483
+ if (prov2) {
484
+ rememberAdmin("province", part);
485
+ out.province = prov2;
486
+ } else if (!out.district) {
487
+ rememberAdmin("district", part);
488
+ out.district = cleanTail(part.slice(key.length - rest.length));
489
+ }
490
+ return;
491
+ }
492
+ const d = RE_DISTRICT.exec(key);
493
+ if (d) {
494
+ markAdmin();
495
+ const rest = d[1] ?? d[2] ?? d[3] ?? "";
496
+ if (!out.district) {
497
+ rememberAdmin("district", part);
498
+ out.district = cleanTail(part.slice(key.length - rest.length));
499
+ }
500
+ return;
501
+ }
502
+ const w = RE_WARD.exec(key);
503
+ if (w) {
504
+ markAdmin();
505
+ const rest = w[1] ?? w[2] ?? w[3] ?? "";
506
+ const at = w[3] !== void 0 ? 0 : key.length - rest.length;
507
+ if (!out.ward) {
508
+ rememberAdmin("ward", part);
509
+ out.ward = cleanTail(part.slice(at, at + rest.length));
510
+ }
511
+ return;
512
+ }
513
+ const prov = lookupProvince(part);
514
+ if (prov) {
515
+ markAdmin();
516
+ rememberAdmin("province", part);
517
+ out.province = prov;
518
+ return;
519
+ }
520
+ if (RE_IGNORE.test(key)) return;
521
+ if (/^\d+$/.test(key)) {
522
+ numbers.push(part);
523
+ return;
524
+ }
525
+ unknown.push({ part, i });
526
+ });
527
+ if (!out.street && unknown.length) {
528
+ const k = unknown.findIndex((u) => u.i > streetIdx && u.i < firstAdminIdx);
529
+ if (k >= 0) {
530
+ const [first] = unknown.splice(k, 1);
531
+ const keep = {
532
+ housenumber: out.housenumber,
533
+ alleyChain: out.alleyChain,
534
+ houseInAlley: out.houseInAlley
535
+ };
536
+ parseStreetPart(first.part, keyOf(first.part), out);
537
+ if (keep.housenumber && !out.housenumber) {
538
+ out.housenumber = keep.housenumber;
539
+ out.alleyChain = keep.alleyChain;
540
+ if (keep.houseInAlley !== void 0) out.houseInAlley = keep.houseInAlley;
541
+ }
542
+ streetIdx = Math.max(streetIdx, first.i);
543
+ }
544
+ }
545
+ for (const u of unknown) {
546
+ if (u.i < streetIdx) continue;
547
+ if (!out.ward) out.ward = u.part;
548
+ else if (!out.district) out.district = u.part;
549
+ }
550
+ for (const n of numbers) {
551
+ if (!out.ward) out.ward = n;
552
+ else if (!out.district) out.district = n;
553
+ }
554
+ out.confidence = Math.round(
555
+ ((out.housenumber ? 0.25 : 0) + (out.street ? 0.35 : 0) + (out.ward ? 0.15 : 0) + (out.district ? 0.1 : 0) + (out.province ? 0.15 : 0)) * 100
556
+ ) / 100;
557
+ return out;
558
+ }
559
+
560
+ // src/admin-alias.ts
561
+ var PREFIX2 = /^(?:tinh|thanh pho|tp|quan|huyen|thi xa|phuong|xa|thi tran)\s+/;
562
+ var prefixOf = (value, fallback) => {
563
+ const normalized = normalizeVi(value ?? "");
564
+ const match = /^(tinh|thanh pho|quan|huyen|thi xa|phuong|xa|thi tran)\s+/.exec(normalized);
565
+ return match?.[1] ?? fallback;
566
+ };
567
+ var coreOf = (value) => normalizeVi(value ?? "").replace(PREFIX2, "");
568
+ function adminAliasKeys(input) {
569
+ const original = input.adminOriginal ?? {};
570
+ const ward = coreOf(input.ward ?? original.ward);
571
+ const district = coreOf(input.district ?? original.district);
572
+ const canonicalProvince = coreOf(input.province);
573
+ const originalProvince = coreOf(original.province);
574
+ const province = ["hcm", "tphcm"].includes(originalProvince) ? canonicalProvince : originalProvince || canonicalProvince;
575
+ const wardPart = ward ? `${prefixOf(original.ward, "phuong")} ${ward}` : "";
576
+ const districtPart = district ? `${prefixOf(original.district, "quan")} ${district}` : "";
577
+ const keys = [];
578
+ const add = (...parts) => {
579
+ const key = parts.filter(Boolean).join(" ");
580
+ if (key && !keys.includes(key)) keys.push(key);
581
+ };
582
+ if (wardPart && districtPart && province) add(wardPart, districtPart, province);
583
+ if (wardPart && districtPart) add(wardPart, districtPart);
584
+ if (wardPart && province) add(wardPart, province);
585
+ if (wardPart) add(wardPart);
586
+ if (ward && !/^\d+$/.test(ward)) add(ward);
587
+ if (districtPart && province) add(districtPart, province);
588
+ if (districtPart) add(districtPart);
589
+ if (district && !/^\d+$/.test(district)) add(district);
590
+ if (originalProvince) add(originalProvince);
591
+ if (canonicalProvince) add(canonicalProvince);
592
+ return keys;
593
+ }
594
+
595
+ // src/style-transform.ts
596
+ var POI_LAYER_ID = "poi";
597
+ var SOVEREIGNTY_LABEL_ID = "sovereignty-label";
598
+ function isPoiStyleLayer(layer) {
599
+ return layer.id === POI_LAYER_ID || layer.source === "poi";
600
+ }
601
+ function nameExpression(lang) {
602
+ return ["coalesce", ["get", `name:${lang}`], ["get", "name"]];
603
+ }
604
+ function isNameLabelLayer(layer) {
605
+ if (layer.type !== "symbol" || layer.id === SOVEREIGNTY_LABEL_ID) return false;
606
+ const textField = layer.layout?.["text-field"];
607
+ return textField !== void 0 && JSON.stringify(textField).includes("name");
608
+ }
609
+ function mapLayers(style, fn) {
610
+ return { ...style, layers: (style.layers ?? []).map(fn) };
611
+ }
612
+ function localizeStyle(style, lang) {
613
+ if (lang === "vi") return style;
614
+ return mapLayers(
615
+ style,
616
+ (layer) => isNameLabelLayer(layer) ? { ...layer, layout: { ...layer.layout, "text-field": nameExpression(lang) } } : layer
617
+ );
618
+ }
619
+ function hidePoiLayer(style) {
620
+ return mapLayers(
621
+ style,
622
+ (layer) => isPoiStyleLayer(layer) ? { ...layer, layout: { ...layer.layout, visibility: "none" } } : layer
623
+ );
624
+ }
625
+
626
+ // src/toponym_alias.json
627
+ var toponym_alias_default = {
628
+ "quy nhon": {
629
+ variants: [
630
+ "qui nhon"
631
+ ],
632
+ source: "osm:node/369487010 alt_name"
633
+ },
634
+ "dak lak": {
635
+ variants: [
636
+ "darlac",
637
+ "daklak"
638
+ ],
639
+ source: "osm:relation/1884034 alt_name"
640
+ },
641
+ "buon ma thuot": {
642
+ variants: [
643
+ "buon me thuot",
644
+ "ban me thuot",
645
+ "buon ma thot"
646
+ ],
647
+ source: "osm:node/2502425156 alt_name"
648
+ },
649
+ "da nang": {
650
+ variants: [
651
+ "tourane"
652
+ ],
653
+ source: "osm:relation/1891418 old_name"
654
+ },
655
+ hue: {
656
+ variants: [
657
+ "thua thien hue",
658
+ "thua thien"
659
+ ],
660
+ source: "osm:relation/1891483 old_name"
661
+ },
662
+ "tan son nhat": {
663
+ variants: [
664
+ "tan son nhut"
665
+ ],
666
+ source: "osm:node/13097604035 old_name"
667
+ },
668
+ "hoi an": {
669
+ variants: [
670
+ "faifo"
671
+ ],
672
+ source: "osm:node/110506070 old_name"
673
+ },
674
+ "soc trang": {
675
+ variants: [
676
+ "khanh hung"
677
+ ],
678
+ source: "osm:node/369487102 old_name"
679
+ },
680
+ "can tho": {
681
+ variants: [
682
+ "tay do",
683
+ "phong dinh"
684
+ ],
685
+ source: "osm:node/948325197 old_name"
686
+ },
687
+ "ca mau": {
688
+ variants: [
689
+ "an xuyen"
690
+ ],
691
+ source: "osm:relation/1873490 old_name"
692
+ },
693
+ "ha noi": {
694
+ variants: [
695
+ "thang long"
696
+ ],
697
+ source: "osm:relation/1903516 old_name"
698
+ }
699
+ };
700
+
701
+ // src/toponym.ts
702
+ var isEntry = (value) => typeof value === "object" && value !== null && "variants" in value && "source" in value;
703
+ var TOPONYM_ALIAS = Object.fromEntries(
704
+ Object.entries(toponym_alias_default).filter(
705
+ (pair) => !pair[0].startsWith("$") && isEntry(pair[1])
706
+ )
707
+ );
708
+ var escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
709
+ var RULES = Object.entries(TOPONYM_ALIAS).flatMap(([canonical, { variants }]) => variants.map((variant) => ({ variant, canonical }))).sort((a, b) => b.variant.length - a.variant.length).map(({ variant, canonical }) => ({
710
+ re: new RegExp(`(^|\\s)${escapeRegExp2(variant)}(?=\\s|$)`, "g"),
711
+ replacement: `$1${canonical}`
712
+ }));
713
+ function applyToponymAlias(normalized) {
714
+ let out = normalized;
715
+ for (const { re, replacement } of RULES) out = out.replace(re, replacement);
716
+ return out;
717
+ }
718
+
719
+ // src/vi_key_rules.json
720
+ var vi_key_rules_default = {
721
+ $comment: [
722
+ "B\u1EA3ng lu\u1EADt kho\xE1 ng\u1EEF \xE2m \u2014 spec 05/09 m\u1EE5c 6.2. \u0110\u1EB7t \u1EDF d\u1EEF li\u1EC7u \u0111\u1EC3 ch\u1EC9nh m\xE0 kh\xF4ng s\u1EEDa logic.",
723
+ "\xC1p SAU normalizeVi v\xE0 applyToponymAlias, theo T\u1EEANG T\u1EEA, r\u1ED3i n\u1ED1i kh\xF4ng kho\u1EA3ng tr\u1EAFng.",
724
+ "Th\u1EE9 t\u1EF1 c\xF3 \xFD ngh\u0129a: \u0111\u1EA7u t\u1EEB \u2192 \xE2m cu\u1ED1i \u2192 i/y. `ngh` ph\u1EA3i tr\u01B0\u1EDBc `gh`; `gi` ph\u1EA3i tr\u01B0\u1EDBc `r`/`d`.",
725
+ "KH\xD4NG \xE1p l/n: 'H\xE0 N\u1ED9i' \u2194 'H\xE0 L\u1ED9i' va ch\u1EA1m t\xEAn ri\xEAng th\u1EADt qu\xE1 nhi\u1EC1u (spec n\xF3i r\xF5).",
726
+ "Ch\u1EC9 d\xF9ng \u1EDF b\u1EADc 3 n\xEAn va ch\u1EA1m ch\u1EC9 \u1EA3nh h\u01B0\u1EDFng th\u1EE9 t\u1EF1 trong nh\xF3m m\u1EDD, kh\xF4ng l\xE0m sai b\u1EADc 1/2."
727
+ ],
728
+ wordStart: [
729
+ ["^ngh", "ng"],
730
+ ["^gh", "g"],
731
+ ["^ph", "f"],
732
+ ["^gi(?=[aeiouy])", "d"],
733
+ ["^r", "d"],
734
+ ["^tr", "c"],
735
+ ["^ch", "c"],
736
+ ["^x", "s"],
737
+ ["^k(?=[aou])", "c"]
738
+ ],
739
+ wordEnd: [
740
+ ["ng$", "n"],
741
+ ["nh$", "n"],
742
+ ["t$", "c"]
743
+ ],
744
+ anywhere: [
745
+ ["quy", "qui"],
746
+ ["([bcdfghklmnpqrstvx])y$", "$1i"]
747
+ ]
748
+ };
749
+
750
+ // src/vi-key.ts
751
+ var RULES2 = vi_key_rules_default;
752
+ var compile = (rules) => rules.map(([pattern, replacement]) => ({ re: new RegExp(pattern), replacement }));
753
+ var WORD_START = compile(RULES2.wordStart);
754
+ var WORD_END = compile(RULES2.wordEnd);
755
+ var ANYWHERE = compile(RULES2.anywhere);
756
+ function viKey(normalized) {
757
+ const words = normalized.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
758
+ return words.map((word) => {
759
+ let w = word;
760
+ for (const { re, replacement } of WORD_START) w = w.replace(re, replacement);
761
+ for (const { re, replacement } of WORD_END) w = w.replace(re, replacement);
762
+ for (const { re, replacement } of ANYWHERE) w = w.replace(re, replacement);
763
+ return w;
764
+ }).join("");
765
+ }
766
+
767
+ // src/search-keys.ts
768
+ function searchKeys(nameNorm, nameAlt) {
769
+ const alts = filterNameAlt(nameNorm, nameAlt);
770
+ return {
771
+ nameKey: viKey(applyToponymAlias(nameNorm)),
772
+ nameAltNorm: alts.length ? alts.map((a) => normalizeVi(a)).join(" | ") : null
773
+ };
774
+ }
775
+ function filterNameAlt(nameNorm, nameAlt) {
776
+ const seen = /* @__PURE__ */ new Set([nameNorm]);
777
+ const out = [];
778
+ for (const raw of nameAlt ?? []) {
779
+ const norm = normalizeVi(raw ?? "");
780
+ if (!norm || seen.has(norm)) continue;
781
+ seen.add(norm);
782
+ out.push(raw);
783
+ }
784
+ return out;
785
+ }
786
+
787
+ // src/telex.ts
788
+ var TELEX_PATTERN = /(aa|ee|oo|dd|[aeiouy][sfrxj]\b|[a-z][1-9]\b)/;
789
+ function looksLikeTelex(normalized) {
790
+ return TELEX_PATTERN.test(normalized);
791
+ }
792
+ function foldTelex(normalized) {
793
+ return normalized.replace(/aa/g, "a").replace(/ee/g, "e").replace(/oo/g, "o").replace(/dd/g, "d").replace(/aw/g, "a").replace(/ow/g, "o").replace(/uw/g, "u").replace(/([aeiouy](?:ch|ng|nh|[cmnpt])?)[sfrxj]\b/g, "$1").replace(/([a-z])[1-9]\b/g, "$1").replace(/\s+/g, " ").trim();
794
+ }
795
+ export {
796
+ ATTRIBUTION_LINKS,
797
+ DEFAULT_POI_SOURCES,
798
+ MapsLibVNError,
799
+ NAME_FILLERS,
800
+ POI_LAYER_ID,
801
+ POI_SOURCES,
802
+ POI_SOURCE_PROFILES,
803
+ TOPONYM_ALIAS,
804
+ adminAliasKeys,
805
+ applyBrandAlias,
806
+ applyToponymAlias,
807
+ attributionHtml,
808
+ attributionText,
809
+ createClient,
810
+ expandAbbrev,
811
+ filterNameAlt,
812
+ foldTelex,
813
+ hidePoiLayer,
814
+ isNameLabelLayer,
815
+ isPoiStyleLayer,
816
+ localizeStyle,
817
+ looksLikeTelex,
818
+ nameCore,
819
+ nameExpression,
820
+ normalizePoiSources,
821
+ normalizeVi,
822
+ parseAddress,
823
+ parsePoiSourcesCsv,
824
+ poiSourceClause,
825
+ poiSourcesKey,
826
+ profileForSources,
827
+ searchKeys,
828
+ stripDiacritics,
829
+ viKey
830
+ };