@whiteslove/parsing-lexicon 0.10.0 → 0.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
package/src/geo.js CHANGED
@@ -19,7 +19,7 @@ export const TASHKENT_DISTRICTS = Object.freeze([
19
19
  entity('Shaykhantahur', { uzLatn: ['Shayxontohur', 'Shayxontohur tumani', 'Shaykhantahur'], uzCyrl: ['Шайхонтоҳур', 'Шайхонтоҳур тумани', 'Шайхонтохур'], ru: ['Шайхантахур', 'Шайхантахурский район'], en: ['Shaykhantahur', 'Shayxontohur'] }),
20
20
  entity('Yunusabad', { uzLatn: ['Yunusobod', 'Yunusobod tumani', 'Yunusabad'], uzCyrl: ['Юнусобод', 'Юнусобод тумани'], ru: ['Юнусабад', 'Юнусабадский район'], en: ['Yunusabad', 'Yunusobod'] }),
21
21
  entity('Yakkasaray', { uzLatn: ['Yakkasaroy', 'Yakkasaroy tumani', 'Yakkasaray'], uzCyrl: ['Яккасарой', 'Яккасарой тумани'], ru: ['Яккасарай', 'Яккасарайский район'], en: ['Yakkasaray', 'Yakkasaroy'] }),
22
- entity('Yangihayot', { uzLatn: ['Yangihayot', 'Yangihayot tumani'], uzCyrl: ['Янгиҳаёт', 'Янгиҳаёт тумани', 'Янгихаёт'], ru: ['Янгихаётский район', 'Янгихаёт'], en: ['Yangihayot'] }),
22
+ entity('Yangihayot', { uzLatn: ['Yangihayot', 'Yangihayot tumani'], uzCyrl: ['Янгиҳаёт', 'Янгиҳаёт тумани', 'Янгихаёт'], ru: ['Янгихаётский район', 'Янгихаёт', 'Янгихаетский район', 'Янгихаетский', 'Янгихает'], en: ['Yangihayot'] }),
23
23
  entity('Yashnobod', { uzLatn: ['Yashnobod', 'Yashnobod tumani', 'Yashnabod', 'Yashnabod tumani', 'Yashnobot', 'Yashnobot tumani'], uzCyrl: ['Яшнобод', 'Яшнобод тумани'], ru: ['Яшнабад', 'Яшнабадский район'], en: ['Yashnobod', 'Yashnabad'] }),
24
24
  ]);
25
25
 
@@ -28,17 +28,21 @@ export function canonicalTashkentDistrict(value) {
28
28
  }
29
29
 
30
30
  function station(name, ru, en, line, aliases = []) {
31
- const allAliases = [...new Set([name, ru, en, ...aliases])];
32
- return deepFreeze({
31
+ const allAliases = Object.freeze([...new Set([name, ru, en, ...aliases])]);
32
+ let re = null;
33
+ return Object.freeze({
33
34
  canonical: name,
34
35
  name,
35
36
  type: 'metro',
36
37
  country: 'UZ',
37
38
  city: 'Tashkent',
38
39
  line,
39
- labels: { ru, en },
40
+ labels: Object.freeze({ ru, en }),
40
41
  aliases: allAliases,
41
- re: aliasesToRegex(allAliases),
42
+ get re() {
43
+ re ||= aliasesToRegex(allAliases);
44
+ return re;
45
+ },
42
46
  });
43
47
  }
44
48
 
@@ -14,10 +14,18 @@ function cityInflectionRegex(aliases) {
14
14
 
15
15
  const CITY_MATCHERS = CITIES.map((item) => {
16
16
  const aliases = [item.canonical, ...aliasesOf(item)];
17
+ let exact = null;
18
+ let inflected;
17
19
  return Object.freeze({
18
20
  item,
19
- exact: aliasesToRegex(aliases),
20
- inflected: cityInflectionRegex(aliases),
21
+ get exact() {
22
+ exact ||= aliasesToRegex(aliases);
23
+ return exact;
24
+ },
25
+ get inflected() {
26
+ if (inflected === undefined) inflected = cityInflectionRegex(aliases);
27
+ return inflected;
28
+ },
21
29
  });
22
30
  });
23
31
 
@@ -27,7 +35,14 @@ const COUNTRY_MATCHERS = COUNTRIES.map((item) => {
27
35
  // are still handled by canonicalCountryCode().
28
36
  const aliases = [item.canonical, ...aliasesOf(item)]
29
37
  .filter((alias) => normalizeForMatch(alias).replace(/\s/g, '').length > 2);
30
- return Object.freeze({ item, re: aliasesToRegex(aliases) });
38
+ let re = null;
39
+ return Object.freeze({
40
+ item,
41
+ get re() {
42
+ re ||= aliasesToRegex(aliases);
43
+ return re;
44
+ },
45
+ });
31
46
  });
32
47
 
33
48
  const US_STATE_RE = /\b[A-Z][A-Za-z.'-]+(?:\s+[A-Z][A-Za-z.'-]+){0,3}\s*,?\s+(?:AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|DC)\b/;
@@ -331,6 +331,8 @@ function profession(canonical, group, strongAliases, weakAliases = []) {
331
331
  const meta = GROUP_META.get(group) || { family: 'other', aliases: [] };
332
332
  const strong = [...new Set([...strongAliases, ...(ROMANIAN_PROFESSION_ALIASES[canonical] || [])].filter(Boolean))];
333
333
  const weak = [...new Set(weakAliases.filter(Boolean))];
334
+ let strongRe = null;
335
+ let weakRe;
334
336
  return Object.freeze({
335
337
  id: canonical,
336
338
  taxonomyId: `profession.${group}.${canonical}`,
@@ -340,8 +342,14 @@ function profession(canonical, group, strongAliases, weakAliases = []) {
340
342
  aliases: Object.freeze(strong),
341
343
  strongAliases: Object.freeze(strong),
342
344
  weakAliases: Object.freeze(weak),
343
- strongRe: aliasesToRegex(strong),
344
- weakRe: weak.length ? aliasesToRegex(weak) : null,
345
+ get strongRe() {
346
+ strongRe ||= aliasesToRegex(strong);
347
+ return strongRe;
348
+ },
349
+ get weakRe() {
350
+ if (weakRe === undefined) weakRe = weak.length ? aliasesToRegex(weak) : null;
351
+ return weakRe;
352
+ },
345
353
  });
346
354
  }
347
355
 
@@ -369,7 +377,15 @@ export const SENIORITY_LEVELS = Object.freeze([
369
377
  { canonical: 'chief', aliases: ['chief officer','chief executive','chief technology','chief product','chief financial','chief marketing','chief operating'], score: 1 },
370
378
  ].map((entry) => {
371
379
  const aliases = [...new Set([...entry.aliases, ...(ROMANIAN_SENIORITY_ALIASES[entry.canonical] || [])])];
372
- return Object.freeze({ ...entry, aliases: Object.freeze(aliases), re: aliasesToRegex(aliases) });
380
+ let re = null;
381
+ return Object.freeze({
382
+ ...entry,
383
+ aliases: Object.freeze(aliases),
384
+ get re() {
385
+ re ||= aliasesToRegex(aliases);
386
+ return re;
387
+ },
388
+ });
373
389
  }));
374
390
 
375
391
  export function matchProfessions(value, { limit = 5, allowWeak = true } = {}) {
@@ -1,13 +1,20 @@
1
1
  import { aliasesToRegex } from './normalization.js';
2
2
  import { matchProfessions } from './hiring-professions.js';
3
3
 
4
- const sourceRole = (canonical, label, aliases, group = 'other') => Object.freeze({
5
- canonical,
6
- label,
7
- group,
8
- aliases: Object.freeze([...new Set(aliases)]),
9
- re: aliasesToRegex([...new Set(aliases)]),
10
- });
4
+ function sourceRole(canonical, label, aliases, group = 'other') {
5
+ const all = Object.freeze([...new Set(aliases)]);
6
+ let re = null;
7
+ return Object.freeze({
8
+ canonical,
9
+ label,
10
+ group,
11
+ aliases: all,
12
+ get re() {
13
+ re ||= aliasesToRegex(all);
14
+ return re;
15
+ },
16
+ });
17
+ }
11
18
 
12
19
  /**
13
20
  * Source spellings seen in real CV feeds which are too source-specific or
@@ -32,7 +32,7 @@ const APPLIANCE_PATTERNS = Object.freeze([
32
32
 
33
33
  const FIRST_RENT_UZ_RE = /(?:hali\s+hech\s+kim\s+(?:yashamagan|turmagan)|ҳали\s+ҳеч\s+ким\s+(?:яшамаган|турмаган))/iu;
34
34
  const LANDLORD_PRESENT_RE = /(?:xozaykali|hojaykali|xo['’]?jaykali|с\s+хозяйк(?:ой|ой\s+в\s+квартире)|хозяйк\p{L}*\s+(?:жив[её]т|прожива\p{L}*)|with\s+(?:the\s+)?(?:landlord|owner)\s+(?:present|living\s+in)|cu\s+proprietar(?:ul)?\s+în\s+cas(?:ă|a)|үй\s*иесі\s+(?:тұрады|бірге\s+тұрады))/iu;
35
- const STUDENT_RE = /(?:studentlar\s+uchun|talabalar\s+uchun|студент(?:ам|ы|ок|ов)?\s+(?:можно|для)|для\s+студент|students?\s+(?:only|welcome)|for\s+students|pentru\s+studen[țt]i|studen[țt]i(?:lor)?|студенттерге|студенттер\s+үшін|(?:oila|oyla)(?:ga|lar|li)?\s+yoki\s+\d{1,2}\s+ta\s+bola(?:lar)?(?:ga)?\s+(?:ijara(?:ga)?\s+)?(?:beril|topshiril))/iu;
35
+ const STUDENT_RE = /(?:studentlar\s+uchun|talabalar\s+uchun|(?<![\p{L}\p{N}_])(?:student(?:ka)?|talaba)(?:lar)?(?:ga)?\s+(?:qiz|yigit|o['’ʻʼ‘`]?g['’ʻʼ‘`]?il\s+bola|bola)(?:lar)?(?:ga)?(?![\p{L}\p{N}_])|(?<![\p{L}\p{N}_])(?:studentlar|talabalar)ga(?![\p{L}\p{N}_])|студент(?:ам|ы|ок|ов)?\s+(?:можно|для)|для\s+студент|students?\s+(?:only|welcome)|for\s+students|pentru\s+studen[țt]i|studen[țt]i(?:lor)?|студенттерге|студенттер\s+үшін|(?:oila|oyla)(?:ga|lar|li)?\s+yoki\s+\d{1,2}\s+ta\s+bola(?:lar)?(?:ga)?\s+(?:ijara(?:ga)?\s+)?(?:beril|topshiril))/iu;
36
36
  const NO_BROKER_RE = /(?:bez\s*makler|maklersiz|vositachisiz|без\s+(?:маклер|посредник|риелтор|риэлтор|комисси)|no\s+(?:broker|agent|commission|agency\s+fee)|f[ăa]r[ăa]\s+(?:comision|agen[țt]ie|intermediari)|делдалсыз|комиссиясыз)/iu;
37
37
  const BROKER_RE = /(?:makler|vositachi|макл(?:ер[а-яё]*)?|ри[еэ]лтор[а-яё]*|агентств[а-яё]*|комисси[а-яё]*|broker|realtor|commission|comision(?:ul)?|agen[țt]ie|delda[lл]\p{L}*|делдал\p{L}*)/iu;
38
38
  const MEN_RE = /(?:o['’ʻʼ‘`]?g['’ʻʼ‘`]?il\s+bola(?:lar)?(?:ga)?|ogil\s+bola(?:lar)?(?:ga)?|sherik\s+bola|эркак(?:лар)?|erkak(?:lar)?(?:ga)?|только\s+(?:мужчин|парн)|\bmen\s+only\b|b[ăa]rba[țt]i(?:lor)?|b[ăa]ie[țt]i(?:lor)?|жігіттерге|жігіттер(?:ге)?|хлопц(?:ям|і|ів)?|чоловік(?:ам|и)?)/iu;
@@ -45,6 +45,10 @@ const FAMILY_EXCLUSION_RE = /(?:oila|oyla|семь\p{L}*|family|famil(?:ie|ia)|c
45
45
  const ROOM_SHARE_RE = /(?:sherik(?:ka|lik|likga)?|шерик(?:ка|лик)?|roommate|flatmate|подселени|койко[-\s]?мест|место\s+в\s+(?:комнат|квартир)|birga\s+yashash(?:ga)?|kvartira(?:ga|da)?[^\r\n.!?]{0,36}(?:\d+|bitta|1)\s*(?:ta\s*)?(?:qiz|ayol)[^\r\n.!?]{0,20}(?:ijarachi\s*)?(?:kerak|kere)|coleg\s+de\s+(?:apartament|camer[ăa])|bed\s+space|бөлмелес(?:\s+керек)?|көрші\s+керек)/iu;
46
46
  const AIR_CONDITIONER_RE = /(?:кондицион|air\s*con|konditsioner|kandit(?:s|c)?aner|kanditsaner|кандитсанер)/iu;
47
47
  const PER_PERSON_PRICE_RE = /(?:kishi\s+boshiga|киши\s+бошига)\s*(\d{1,3}(?:[\s.,]\d{3})*|\d+(?:[.,]\d+)?)\s*(ming|минг|million|mln|млн)?(?:dan|дан)?/iu;
48
+ // Shared-flat posts price each tenant with the ablative "-dan" ("800 000
49
+ // so'mdan" = "800 000 so'm from each") once they ask for several people.
50
+ const PER_PERSON_SUFFIX_PRICE_RE = /(\d{1,3}(?:[  ]\d{3})+|\d+)\s*(ming|минг|million|mln|млн)?\s*(?:so['’ʻʼ‘`]?m|сўм|сум)(?:dan|дан)(?![\p{L}\p{N}_])/iu;
51
+ const SEVERAL_OCCUPANTS_RE = /(?<![\p{L}\p{N}_])([2-9])\s*ta\s+(?:qiz|yigit|bola|kishi|odam|talaba|student)\p{L}*\s+(?:kerak|kere|olinadi)/iu;
48
52
  const WALK_MINUTES_RE = /(?:yayov|piyoda|пешком)\s*(\d{1,2})\s*(?:daqiqa|min(?:ute)?s?|минут)/iu;
49
53
  const TRANSIT_ROUTES_RE = /(?:aftobuslar|avtobuslar|автобуслар|автобусы)[^\r\n\d]{0,24}((?:\d{1,4}[\s,;/]*){1,10})/iu;
50
54
  const NEARBY_RELATION_TAIL_RE = /(?<!\p{L})(?:рядом\s+(?:с|со)|недалеко\s+от|возле|около|напротив|навпроти|ориентир\s*[:—–-]?|ор[-–—]?р\.?\s*[:—–-]?|near(?:by)?|close\s+to|next\s+to|opposite|behind|in\s+front\s+of|yaqin(?:ida)?|lângă|aproape\s+de|în\s+apropiere\s+de|vizavi\s+de|în\s+spatele|în\s+fața)(?!\p{L})[^.!?\r\n;]*/giu;
@@ -194,7 +198,9 @@ export function parseHousingAudience(value) {
194
198
  const students = STUDENT_RE.test(text);
195
199
  if (family && students) return deepFreeze({ primary: 'family', alternatives: ['family', 'students'] });
196
200
  if (family && women) return deepFreeze({ primary: 'family', alternatives: ['family', 'women'] });
197
- if (students) return deepFreeze({ primary: 'students', alternatives: ['students'] });
201
+ // "Student qizlarga" narrows students to girls: the gender is the binding
202
+ // restriction, and studentTarget still reports the student part.
203
+ if (students && !women && !men) return deepFreeze({ primary: 'students', alternatives: ['students'] });
198
204
  if (family) return deepFreeze({ primary: 'family', alternatives: ['family'] });
199
205
  if (women) return deepFreeze({ primary: 'women', alternatives: ['women'] });
200
206
  if (men) return deepFreeze({ primary: 'men', alternatives: ['men'] });
@@ -222,14 +228,15 @@ export function parseHousingCommission(value) {
222
228
 
223
229
  export function parseHousingPerPersonPrice(value, { country = '' } = {}) {
224
230
  const text = normalizeUnicode(value ?? '');
225
- const match = text.match(PER_PERSON_PRICE_RE);
231
+ const match = text.match(PER_PERSON_PRICE_RE)
232
+ || (SEVERAL_OCCUPANTS_RE.test(text) ? text.match(PER_PERSON_SUFFIX_PRICE_RE) : null);
226
233
  if (!match) return null;
227
234
  let amount = Number(String(match[1]).replace(/\s+/g, '').replace(',', '.'));
228
235
  if (!Number.isFinite(amount) || amount <= 0) return null;
229
236
  const scale = String(match[2] || '').toLowerCase();
230
237
  if (scale === 'ming' || scale === 'минг') amount *= 1000;
231
238
  else if (scale === 'million' || scale === 'mln' || scale === 'млн') amount *= 1_000_000;
232
- const currency = String(country || '').toUpperCase() === 'UZ' || /(?:ming|минг)/iu.test(match[0]) ? 'UZS' : null;
239
+ const currency = String(country || '').toUpperCase() === 'UZ' || /(?:ming|минг|so['’ʻʼ‘`]?m|сўм|сум)/iu.test(match[0]) ? 'UZS' : null;
233
240
  const approximate = /около|примерно|~|≈/iu.test(text);
234
241
  return deepFreeze({ amount, currency, approximate, scope: 'person' });
235
242
  }
@@ -602,7 +602,10 @@ export function parseHousingPrice(value, context = '') {
602
602
  const hasExplicitUzbekThousandScale = country === 'UZ' && /\b(?:ming|минг)\b/iu.test(priceText);
603
603
  if (!explicit && !hasExplicitUzbekThousandScale && fallbackCurrency === 'UZS' && price != null) {
604
604
  const dailyUzbek = /(?:kunlik|sutkaga|kecha[- ]?kunduz|посуточн|суточн)/i.test(priceText);
605
- currency = price >= 1_000_000 || (dailyUzbek && price >= 10_000) ? 'UZS' : 'USD';
605
+ // A monthly rent of 100,000+ is only plausible in so'm; the USD reading
606
+ // turned "800 000" per person into an 800,000 USD rent.
607
+ const monthlyRent = dealType === 'longRent' && price >= 100_000;
608
+ currency = price >= 1_000_000 || monthlyRent || (dailyUzbek && price >= 10_000) ? 'UZS' : 'USD';
606
609
  }
607
610
 
608
611
  return Object.freeze({ amount: price, currency, approximate: price != null && APPROXIMATE_RE.test(priceText) });
@@ -87,7 +87,9 @@ export function parseHousingRoomCount(value) {
87
87
  if (total >= 1 && total <= 20) return total;
88
88
  }
89
89
  for (const [re, rooms] of NUMBER_WORDS) if (re.test(text)) return rooms;
90
- const numeric = text.match(/(?:^|[^\p{L}\p{N}])(\d{1,2})\s*(?:ta\s*)?(?:(?:-\s*)?комнат\p{L}*|(?:-\s*)?к(?:\.|\b)|(?:-\s*)?xona(?:li|si|lari)?|(?:-\s*)?хона(?:лик|ли|си|лари)?|бөлмелі|rooms?)(?=$|[^\p{L}\p{N}])/iu);
90
+ // "5-х комн.кв" / "3-ех комнатная": a Russian case ending may sit between
91
+ // the number and the room word, which may itself be abbreviated.
92
+ const numeric = text.match(/(?:^|[^\p{L}\p{N}])(\d{1,2})\s*(?:-\s*(?:х|ти|и|ух|[её]х)\s*)?(?:ta\s*)?(?:(?:-\s*)?комн(?:ат)?\p{L}*|(?:-\s*)?к(?:\.|\b)|(?:-\s*)?xona(?:li|si|lari)?|(?:-\s*)?хона(?:лик|ли|си|лари)?|бөлмелі|rooms?)(?=$|[^\p{L}\p{N}])/iu);
91
93
  if (numeric) {
92
94
  const rooms = toNumber(numeric[1]);
93
95
  if (rooms != null && rooms >= 1 && rooms <= 20) return rooms;
@@ -127,7 +129,8 @@ export function parseHousingFloor(value) {
127
129
  }
128
130
  }
129
131
 
130
- const beforeMarker = text.match(/(?:^|[^\d])(\d{1,3})\s*-?\s*(?:(?:chi|чи)\s*)?(?:этаж(?:да)?|поверх|floor|etaj|qavat(?:i(?:da(?:gi)?)?|da)?|қабат(?:ы(?:нда(?:ғы)?)?|та)?|кават(?:и(?:да(?:ги)?)?|да)?|қават(?:и(?:да(?:ги)?)?|да)?)(?=$|[^\p{L}\p{N}_])/iu);
132
+ // "3-й этаж" / "5-м этаже" carry a Russian ordinal ending like Uzbek "-chi".
133
+ const beforeMarker = text.match(/(?:^|[^\d])(\d{1,3})\s*-?\s*(?:(?:chi|чи|й|ий|ый|ой|м|ом|ем)\s*)?(?:этаж(?:да)?|поверх|floor|etaj|qavat(?:i(?:da(?:gi)?)?|da)?|қабат(?:ы(?:нда(?:ғы)?)?|та)?|кават(?:и(?:да(?:ги)?)?|да)?|қават(?:и(?:да(?:ги)?)?|да)?)(?=$|[^\p{L}\p{N}_])/iu);
131
134
  let floor = toNumber(beforeMarker?.[1]);
132
135
  let totalFloors = null;
133
136
 
@@ -140,7 +143,7 @@ export function parseHousingFloor(value) {
140
143
  if (floor == null && FIRST_FLOOR_WORD_RE.test(text)) floor = 1;
141
144
 
142
145
  if (totalFloors == null) {
143
- const total = text.match(/(?:дом\s*)?(\d{1,3})\s*(?:[- ]?этаж(?:н\p{L}*|лик)|поверхов\p{L}*|storey|story|floors?\s+total|qavatli|қабатты|каватли|қаватли)/iu);
146
+ const total = text.match(/(?:дом\s*)?(\d{1,3})\s*(?:-\s*(?:ти|х|и|ми)\s*)?(?:[- ]?этаж(?:н\p{L}*|лик)|поверхов\p{L}*|storey|story|floors?\s+total|qavatli|қабатты|каватли|қаватли)/iu);
144
147
  totalFloors = toNumber(total?.[1]);
145
148
  }
146
149
 
@@ -167,7 +170,7 @@ const AREA_UNIT_RE = String.raw`(?:м²|м2|m²|m2|sqm|sq\.?\s*m|mp|кв\.?\s*м
167
170
  const AREA_UNIT_AFTER_LABEL_RE = String.raw`(?:${AREA_UNIT_RE}|[mм])`;
168
171
 
169
172
  function areaAfterLabel(text, labelRe) {
170
- const re = new RegExp(`${labelRe.source}\\s*[:=-]?\\s*(\\d{1,4}(?:[.,]\\d{1,2})?)\\s*${AREA_UNIT_AFTER_LABEL_RE}(?=$|[^\\p{L}\\p{N}_])`, 'iu');
173
+ const re = new RegExp(`${labelRe.source}\\s*[:=-]?\\s*(\\d{1,4}(?:[.,]\\d{1,2})?)\\s*-?\\s*${AREA_UNIT_AFTER_LABEL_RE}(?=$|[^\\p{L}\\p{N}_])`, 'iu');
171
174
  return toNumber(text.match(re)?.[1]);
172
175
  }
173
176
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(name, aliases = [], meta = {}) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  ...meta,
7
8
  canonical: name,
@@ -10,7 +11,10 @@ function entry(name, aliases = [], meta = {}) {
10
11
  entityType: meta.entityType || meta.type,
11
12
  country: 'KG',
12
13
  aliases: all,
13
- re: aliasesToRegex(all),
14
+ get re() {
15
+ re ||= aliasesToRegex(all);
16
+ return re;
17
+ },
14
18
  });
15
19
  }
16
20
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(name, aliases, entityType) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function entry(name, aliases, entityType) {
9
10
  entityType,
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function residential(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function residential(name, aliases = []) {
9
10
  entityType: 'residential_complex',
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function street(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function street(name, aliases = []) {
9
10
  entityType: 'street',
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function residential(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function residential(name, aliases = []) {
9
10
  entityType: 'residential_complex',
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,7 +2,18 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(name, aliases = [], meta = {}) {
4
4
  const all = [...new Set([name, ...aliases].filter(Boolean))];
5
- return Object.freeze({ ...meta, canonical: name, name, type: meta.type || meta.entityType, aliases: Object.freeze(all), re: aliasesToRegex(all) });
5
+ let re = null;
6
+ return Object.freeze({
7
+ ...meta,
8
+ canonical: name,
9
+ name,
10
+ type: meta.type || meta.entityType,
11
+ aliases: Object.freeze(all),
12
+ get re() {
13
+ re ||= aliasesToRegex(all);
14
+ return re;
15
+ },
16
+ });
6
17
  }
7
18
 
8
19
  function entries(rows = [], defaults = {}) {
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(name, aliases, entityType) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function entry(name, aliases, entityType) {
9
10
  entityType,
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function street(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function street(name, aliases = []) {
9
10
  entityType: 'street',
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function residential(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function residential(name, aliases = []) {
9
10
  entityType: 'residential_complex',
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function residential(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function residential(name, aliases = []) {
9
10
  entityType: 'residential_complex',
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(name, aliases = [], entityType) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function entry(name, aliases = [], entityType) {
9
10
  entityType,
10
11
  country: 'KZ',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -5,14 +5,35 @@ import { UA_REGIONS as UA_REGION_LEXICON } from './geography.js';
5
5
  function entries(rows) {
6
6
  return Object.freeze(rows.map(([name, ...aliases]) => {
7
7
  const all = [...new Set([name, ...aliases].filter(Boolean))];
8
- return Object.freeze({ canonical: name, name, aliases: Object.freeze(all), re: aliasesToRegex(all) });
8
+ let re = null;
9
+ return Object.freeze({
10
+ canonical: name,
11
+ name,
12
+ aliases: Object.freeze(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
17
+ });
9
18
  }));
10
19
  }
11
20
 
12
21
  function lexiconEntries(items) {
13
22
  return Object.freeze((items || []).map((item) => {
14
23
  const all = [...new Set([item.canonical, ...aliasesOf(item)].filter(Boolean))];
15
- return Object.freeze({ canonical: item.canonical, name: item.canonical, type: item.type, country: item.country, city: item.city, aliases: Object.freeze(all), re: aliasesToRegex(all) });
24
+ let re = null;
25
+ return Object.freeze({
26
+ canonical: item.canonical,
27
+ name: item.canonical,
28
+ type: item.type,
29
+ country: item.country,
30
+ city: item.city,
31
+ aliases: Object.freeze(all),
32
+ get re() {
33
+ re ||= aliasesToRegex(all);
34
+ return re;
35
+ },
36
+ });
16
37
  }));
17
38
  }
18
39
 
package/src/locations.js CHANGED
@@ -115,6 +115,7 @@ const UZ_BASE_LOCATION_DICTIONARIES = Object.freeze({
115
115
 
116
116
  function semanticEntry(entry, name, aliases, entityType) {
117
117
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
118
+ let re = null;
118
119
  return Object.freeze({
119
120
  ...entry,
120
121
  canonical: name,
@@ -122,7 +123,10 @@ function semanticEntry(entry, name, aliases, entityType) {
122
123
  type: entityType,
123
124
  entityType,
124
125
  aliases: all,
125
- re: aliasesToRegex(all),
126
+ get re() {
127
+ re ||= aliasesToRegex(all);
128
+ return re;
129
+ },
126
130
  });
127
131
  }
128
132
 
@@ -179,13 +183,17 @@ function normalizeUzSemanticLocations(country) {
179
183
  'Корасув дахаси',
180
184
  'Карасу даха',
181
185
  ])]);
186
+ let qorasuvRe = null;
182
187
  const qorasuvArea = Object.freeze({
183
188
  ...qorasuv,
184
189
  type: 'local_area',
185
190
  entityType: 'local_area',
186
191
  parent: 'Mirzo Ulugbek',
187
192
  aliases: qorasuvAliases,
188
- re: aliasesToRegex(qorasuvAliases),
193
+ get re() {
194
+ qorasuvRe ||= aliasesToRegex(qorasuvAliases);
195
+ return qorasuvRe;
196
+ },
189
197
  });
190
198
 
191
199
  normalized = Object.freeze({
@@ -5,7 +5,7 @@ const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, alias
5
5
  export const CURRENCY_TERMS = Object.freeze([
6
6
  group('USD', { ru: ['$', 'usd', 'доллар', 'доллара', 'долларов', 'бакс', 'у.е.', 'у е'], en: ['$', 'usd', 'dollar', 'dollars', 'us dollar'], uk: ['$', 'usd', 'долар', 'долари', 'доларів'], ro: ['$', 'usd', 'dolar', 'dolari'], uzLatn: ['$', 'usd', 'dollar'], uzCyrl: ['$', 'доллар'], kk: ['$', 'usd', 'доллар'] }),
7
7
  group('EUR', { ru: ['€', 'eur', 'евро'], en: ['€', 'eur', 'euro', 'euros'], uk: ['€', 'eur', 'євро'], ro: ['€', 'eur', 'euro'], uzLatn: ['€', 'eur', 'yevro'], uzCyrl: ['€', 'евро'], kk: ['€', 'eur', 'еуро'] }),
8
- group('UZS', { ru: ['сум', 'сумов', 'узс'], en: ['uzs', 'uzbek som'], uk: ['uzs', 'сум'], ro: ['uzs'], uzLatn: ["so'm", 'so‘m', 'soʻm', 'sum', 'uzs'], uzCyrl: ['сўм', 'сум'], kk: ['uzs'] }),
8
+ group('UZS', { ru: ['сум', 'сумов', 'узс'], en: ['uzs', 'uzbek som'], uk: ['uzs', 'сум'], ro: ['uzs'], uzLatn: ["so'm", 'so‘m', 'soʻm', 'sum', 'uzs', "so'mdan", 'so‘mdan', 'soʻmdan', 'somdan', 'sumdan', "so'mgacha", 'so‘mgacha', 'soʻmgacha'], uzCyrl: ['сўм', 'сум', 'сўмдан', 'сумдан', 'сўмгача'], kk: ['uzs'] }),
9
9
  group('KZT', { ru: ['₸', 'тенге', 'тг', 'kzt'], en: ['₸', 'kzt', 'tenge'], uk: ['₸', 'тенге', 'kzt'], ro: ['₸', 'kzt', 'tenge'], uzLatn: ['₸', 'kzt', 'tenge'], uzCyrl: ['₸', 'kzt', 'тенге'], kk: ['₸', 'теңге', 'тенге', 'тг', 'kzt'] }),
10
10
  group('UAH', { ru: ['грн', 'гривна', 'гривны', 'гривен', '₴', 'uah'], en: ['uah', 'hryvnia', 'hryvnias', '₴'], uk: ['грн', 'гривня', 'гривні', 'гривень', '₴', 'uah'], ro: ['uah', 'hrivne', 'grivne'], uzLatn: ['uah'], uzCyrl: ['uah'], kk: ['uah'] }),
11
11
  group('RUB', { ru: ['₽', 'руб', 'руб.', 'рубль', 'рубля', 'рублей', 'rub', 'rur'], en: ['₽', 'rub', 'rur', 'ruble', 'rubles'], uk: ['₽', 'руб', 'рублів', 'rub'], ro: ['₽', 'rub', 'ruble'], uzLatn: ['₽', 'rub', 'rubl'], uzCyrl: ['₽', 'рубль'], kk: ['₽', 'rub', 'рубль'] }),
@@ -2,12 +2,16 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entity(name, type, aliases = [], options = {}) {
4
4
  const all = [...new Set([name, ...aliases].filter(Boolean))];
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
8
9
  type,
9
10
  aliases: Object.freeze(all),
10
- re: aliasesToRegex(all),
11
+ get re() {
12
+ re ||= aliasesToRegex(all);
13
+ return re;
14
+ },
11
15
  parent: options.parent || null,
12
16
  cluster: options.cluster || null,
13
17
  contextRequired: Boolean(options.contextRequired),
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function gastronomicStreet(canonical, aliases = [], district = null) {
4
4
  const all = Object.freeze([...new Set([canonical, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical,
7
8
  name: canonical,
@@ -11,7 +12,10 @@ function gastronomicStreet(canonical, aliases = [], district = null) {
11
12
  city: 'Tashkent',
12
13
  district,
13
14
  aliases: all,
14
- re: aliasesToRegex(all),
15
+ get re() {
16
+ re ||= aliasesToRegex(all);
17
+ return re;
18
+ },
15
19
  });
16
20
  }
17
21
 
@@ -3,6 +3,7 @@ import { TASHKENT_DISTRICTS, TASHKENT_METRO } from './geo.js';
3
3
 
4
4
  function locationEntry(name, category, aliases = [], options = {}) {
5
5
  const all = [...new Set([name, ...aliases].filter(Boolean))];
6
+ let re = null;
6
7
  return Object.freeze({
7
8
  canonical: name,
8
9
  name,
@@ -10,7 +11,10 @@ function locationEntry(name, category, aliases = [], options = {}) {
10
11
  country: 'UZ',
11
12
  city: 'Tashkent',
12
13
  aliases: Object.freeze(all),
13
- re: aliasesToRegex(all),
14
+ get re() {
15
+ re ||= aliasesToRegex(all);
16
+ return re;
17
+ },
14
18
  contextRequired: Boolean(options.contextRequired),
15
19
  contextRe: options.context ? new RegExp(options.context, 'iu') : null,
16
20
  });
@@ -18,6 +22,7 @@ function locationEntry(name, category, aliases = [], options = {}) {
18
22
 
19
23
  const TASHKENT_HOUSING_DISTRICTS = Object.freeze(TASHKENT_DISTRICTS.map((district) => {
20
24
  const aliases = [...new Set([district.canonical, ...aliasesOf(district)].filter(Boolean))];
25
+ let re = null;
21
26
  return Object.freeze({
22
27
  canonical: district.canonical,
23
28
  name: district.canonical,
@@ -25,7 +30,10 @@ const TASHKENT_HOUSING_DISTRICTS = Object.freeze(TASHKENT_DISTRICTS.map((distric
25
30
  country: 'UZ',
26
31
  city: 'Tashkent',
27
32
  aliases: Object.freeze(aliases),
28
- re: aliasesToRegex(aliases),
33
+ get re() {
34
+ re ||= aliasesToRegex(aliases);
35
+ return re;
36
+ },
29
37
  });
30
38
  }));
31
39
 
@@ -167,7 +175,7 @@ export const TASHKENT_NUMBERED_AREA_ALIASES = Object.freeze({
167
175
  Kuylyuk: Object.freeze(['куйлюк', 'куйлик', 'kuylyuk', 'kuyliq', 'qoyliq', 'qo yliq']),
168
176
  Sergeli: Object.freeze(['сергели', 'sergeli', 'sergile', 'sergele']),
169
177
  Yunusabad: Object.freeze(['юнусабад', 'yunusabad', 'yunusobod']),
170
- Yangihayot: Object.freeze(['янгихаёт', 'янгихаят', 'yangihayot']),
178
+ Yangihayot: Object.freeze(['янгихаёт', 'янгихает', 'янгихаят', 'yangihayot']),
171
179
  });
172
180
 
173
181
  const REVERSE_NUMBERED_ALIASES = Object.freeze({
@@ -241,15 +249,24 @@ export function hasExplicitTashkentDistrict(value, canonical) {
241
249
  export function matchTashkentHousingDistrict(value) {
242
250
  const text = String(value ?? '');
243
251
  if (!text) return null;
252
+ let explicit = null;
253
+ let implicit = null;
244
254
  for (const district of TASHKENT_HOUSING_DISTRICTS) {
245
255
  const match = text.match(district.re);
246
256
  if (!match) continue;
247
- if (hasExplicitDistrictContext(text, match)) return district;
257
+ // The earliest explicitly marked district wins. In "Янгихаетский район
258
+ // Сергели-1" the "район" also precedes Сергели, but it belongs to the
259
+ // adjective before it; Sergeli-1 is only the quarter name.
260
+ if (hasExplicitDistrictContext(text, match)) {
261
+ if (!explicit || match.index < explicit.index) explicit = { district, index: match.index };
262
+ continue;
263
+ }
264
+ if (implicit) continue;
248
265
  if (hasExplicitMetroContext(text, match) || hasExplicitAreaContext(text, match) || hasExplicitMahallaContext(text, match)) continue;
249
266
  if (matchTashkentNumberedArea(text, district.name)) continue;
250
- return district;
267
+ implicit = district;
251
268
  }
252
- return null;
269
+ return explicit?.district || implicit;
253
270
  }
254
271
 
255
272
  /**
@@ -2,13 +2,17 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function poi(name, category, aliases = [], options = {}) {
4
4
  const all = [...new Set([name, ...aliases].filter(Boolean))];
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
8
9
  category,
9
10
  categories: Object.freeze([...new Set([category, ...(options.categories || [])])]),
10
11
  aliases: Object.freeze(all),
11
- re: aliasesToRegex(all),
12
+ get re() {
13
+ re ||= aliasesToRegex(all);
14
+ return re;
15
+ },
12
16
  contextRequired: Boolean(options.contextRequired),
13
17
  contextRe: options.context ? new RegExp(options.context, 'iu') : null,
14
18
  ...(options.parent ? { parent: options.parent } : {}),
@@ -12,12 +12,16 @@ const AMBIGUOUS_TASHKENT_COMPLEXES = new Set([
12
12
 
13
13
  function complex(name, aliases = []) {
14
14
  const all = [...new Set([name, ...aliases].filter(Boolean))];
15
+ let re = null;
15
16
  return Object.freeze({
16
17
  canonical: name,
17
18
  name,
18
19
  aliases: Object.freeze(all),
19
20
  ambiguous: AMBIGUOUS_TASHKENT_COMPLEXES.has(name),
20
- re: aliasesToRegex(all),
21
+ get re() {
22
+ re ||= aliasesToRegex(all);
23
+ return re;
24
+ },
21
25
  });
22
26
  }
23
27
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function residential(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function residential(name, aliases = []) {
9
10
  entityType: 'residential_complex',
10
11
  country: 'UA',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function residential(name, aliases = []) {
4
4
  const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical: name,
7
8
  name,
@@ -9,7 +10,10 @@ function residential(name, aliases = []) {
9
10
  entityType: 'residential_complex',
10
11
  country: 'UA',
11
12
  aliases: all,
12
- re: aliasesToRegex(all),
13
+ get re() {
14
+ re ||= aliasesToRegex(all);
15
+ return re;
16
+ },
13
17
  });
14
18
  }
15
19
 
@@ -2,7 +2,18 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(name, aliases = [], meta = {}) {
4
4
  const all = [...new Set([name, ...aliases].filter(Boolean))];
5
- return Object.freeze({ ...meta, canonical: name, name, type: meta.type || meta.entityType, aliases: Object.freeze(all), re: aliasesToRegex(all) });
5
+ let re = null;
6
+ return Object.freeze({
7
+ ...meta,
8
+ canonical: name,
9
+ name,
10
+ type: meta.type || meta.entityType,
11
+ aliases: Object.freeze(all),
12
+ get re() {
13
+ re ||= aliasesToRegex(all);
14
+ return re;
15
+ },
16
+ });
6
17
  }
7
18
 
8
19
  function entries(rows = [], defaults = {}) {
@@ -2,6 +2,7 @@ import { aliasesToRegex } from './normalization.js';
2
2
 
3
3
  function entry(canonical, type, aliases = [], labels = null) {
4
4
  const all = Object.freeze([...new Set([canonical, ...aliases].filter(Boolean))]);
5
+ let re = null;
5
6
  return Object.freeze({
6
7
  canonical,
7
8
  name: canonical,
@@ -11,7 +12,10 @@ function entry(canonical, type, aliases = [], labels = null) {
11
12
  city: 'Samarkand',
12
13
  ...(labels ? { labels: Object.freeze(labels) } : {}),
13
14
  aliases: all,
14
- re: aliasesToRegex(all),
15
+ get re() {
16
+ re ||= aliasesToRegex(all);
17
+ return re;
18
+ },
15
19
  });
16
20
  }
17
21
 
@@ -11,6 +11,7 @@ function legacyAreaEntry(district, canonical) {
11
11
  ...(source.aliases || []),
12
12
  ].filter(Boolean))]);
13
13
 
14
+ let re = null;
14
15
  return Object.freeze({
15
16
  canonical: source.name,
16
17
  name: source.name,
@@ -21,12 +22,16 @@ function legacyAreaEntry(district, canonical) {
21
22
  district,
22
23
  parent: district,
23
24
  aliases,
24
- re: aliasesToRegex(aliases),
25
+ get re() {
26
+ re ||= aliasesToRegex(aliases);
27
+ return re;
28
+ },
25
29
  });
26
30
  }
27
31
 
28
32
  function residentialEntry(canonical, aliases = []) {
29
33
  const all = Object.freeze([...new Set([canonical, ...aliases].filter(Boolean))]);
34
+ let re = null;
30
35
  return Object.freeze({
31
36
  canonical,
32
37
  name: canonical,
@@ -35,7 +40,10 @@ function residentialEntry(canonical, aliases = []) {
35
40
  country: 'UZ',
36
41
  city: 'Tashkent',
37
42
  aliases: all,
38
- re: aliasesToRegex(all),
43
+ get re() {
44
+ re ||= aliasesToRegex(all);
45
+ return re;
46
+ },
39
47
  });
40
48
  }
41
49