@whiteslove/parsing-lexicon 0.2.0 → 0.2.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.
Files changed (94) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +137 -137
  3. package/index.d.ts +374 -374
  4. package/package.json +77 -75
  5. package/src/central-asia-locations.js +211 -211
  6. package/src/central-asia.js +233 -233
  7. package/src/contact.d.ts +44 -44
  8. package/src/contact.js +159 -159
  9. package/src/countries.js +175 -175
  10. package/src/country-context.d.ts +10 -10
  11. package/src/country-context.js +22 -22
  12. package/src/currency.d.ts +14 -14
  13. package/src/currency.js +66 -66
  14. package/src/geo.js +169 -169
  15. package/src/geography-central-asia.js +42 -42
  16. package/src/geography-detection.d.ts +14 -14
  17. package/src/geography-detection.js +64 -64
  18. package/src/geography-display.d.ts +13 -13
  19. package/src/geography-display.js +113 -113
  20. package/src/geography.js +210 -210
  21. package/src/hiring-advanced.js +147 -147
  22. package/src/hiring-candidate-fields.d.ts +9 -9
  23. package/src/hiring-candidate-fields.js +92 -92
  24. package/src/hiring-context.d.ts +64 -64
  25. package/src/hiring-context.js +412 -412
  26. package/src/hiring-language-extensions.d.ts +19 -19
  27. package/src/hiring-language-extensions.js +139 -139
  28. package/src/hiring-languages.js +135 -135
  29. package/src/hiring-location-fields.d.ts +3 -3
  30. package/src/hiring-location-fields.js +53 -53
  31. package/src/hiring-professions-ro.js +191 -191
  32. package/src/hiring-professions.js +437 -437
  33. package/src/hiring-requirements.d.ts +16 -16
  34. package/src/hiring-requirements.js +170 -170
  35. package/src/hiring-salary-context.d.ts +27 -27
  36. package/src/hiring-salary-context.js +76 -76
  37. package/src/hiring-semantics.d.ts +29 -29
  38. package/src/hiring-semantics.js +218 -218
  39. package/src/hiring-skills.d.ts +21 -21
  40. package/src/hiring-skills.js +315 -315
  41. package/src/hiring-source-aliases.d.ts +32 -32
  42. package/src/hiring-source-aliases.js +185 -185
  43. package/src/hiring-source-semantics.d.ts +31 -31
  44. package/src/hiring-source-semantics.js +293 -293
  45. package/src/hiring-temporal.d.ts +15 -15
  46. package/src/hiring-temporal.js +67 -67
  47. package/src/hiring-vacancy-fields.d.ts +5 -5
  48. package/src/hiring-vacancy-fields.js +23 -23
  49. package/src/hiring-work-semantics.d.ts +34 -34
  50. package/src/hiring-work-semantics.js +72 -72
  51. package/src/hiring.js +180 -180
  52. package/src/housing-address.d.ts +18 -18
  53. package/src/housing-address.js +217 -210
  54. package/src/housing-commercial.d.ts +1 -0
  55. package/src/housing-commercial.js +9 -0
  56. package/src/housing-context.d.ts +36 -36
  57. package/src/housing-context.js +183 -183
  58. package/src/housing-features.js +30 -30
  59. package/src/housing-intent.d.ts +9 -9
  60. package/src/housing-intent.js +132 -132
  61. package/src/housing-listing-fields.js +131 -131
  62. package/src/housing-money.d.ts +7 -7
  63. package/src/housing-money.js +102 -102
  64. package/src/housing-source-aliases.d.ts +8 -8
  65. package/src/housing-source-aliases.js +51 -51
  66. package/src/housing-structured.d.ts +55 -55
  67. package/src/housing-structured.js +219 -219
  68. package/src/housing.js +200 -200
  69. package/src/index.js +45 -44
  70. package/src/kz-location-extensions.js +307 -307
  71. package/src/landmarks.js +21 -21
  72. package/src/lexicon-core.js +58 -58
  73. package/src/location-merge.js +106 -106
  74. package/src/locations.js +456 -456
  75. package/src/money-core.d.ts +10 -10
  76. package/src/money-core.js +81 -81
  77. package/src/money-lexicon.d.ts +5 -5
  78. package/src/money-lexicon.js +79 -79
  79. package/src/money.js +154 -154
  80. package/src/normalization.js +384 -384
  81. package/src/odesa-metropolitan.js +144 -144
  82. package/src/romania-geography.js +47 -47
  83. package/src/tashkent-colloquial.js +80 -72
  84. package/src/tashkent-housing-geography.d.ts +38 -38
  85. package/src/tashkent-housing-geography.js +211 -211
  86. package/src/tashkent-pois.js +215 -215
  87. package/src/tashkent-residential-complexes.js +272 -272
  88. package/src/ua-location-extensions-major.js +118 -118
  89. package/src/ua-location-extensions-metro.js +9 -9
  90. package/src/ua-location-extensions-regional.js +246 -246
  91. package/src/ua-secondary-cities.d.ts +2 -2
  92. package/src/ua-secondary-cities.js +57 -57
  93. package/src/ukraine.js +75 -75
  94. package/src/uz-location-extensions.js +259 -259
package/src/landmarks.js CHANGED
@@ -1,21 +1,21 @@
1
- import { lexiconEntity } from './lexicon-core.js';
2
- const group = (canonical, aliases) => lexiconEntity(canonical, aliases);
3
-
4
- export const GENERIC_LANDMARK_TERMS = Object.freeze([
5
- group('Park', { ru: ['парк'], en: ['park'], uk: ['парк'], ro: ['parc'], uzLatn: ["bog'", 'bog‘', 'bogʻ', 'park'], uzCyrl: ['боғ', 'парк'], kk: ['саябақ', 'парк'] }),
6
- group('Metro', { ru: ['метро', 'станция метро'], en: ['metro', 'subway station'], uk: ['метро', 'станція метро'], ro: ['metrou', 'stație de metrou'], uzLatn: ['metro', 'metro bekati'], uzCyrl: ['метро', 'метро бекати'], kk: ['метро', 'метро станциясы'] }),
7
- group('Bus stop', { ru: ['автобусная остановка', 'остановка автобуса', 'конечная автобуса'], en: ['bus stop', 'bus station'], uk: ['автобусна зупинка', 'зупинка автобуса'], ro: ['stație de autobuz', 'statie de autobuz'], uzLatn: ['avtobus bekati', 'avtobus kanichka', 'avtobus kanichkasi', 'avtobus konichka', 'avtobus konichkasi'], uzCyrl: ['автобус бекати'], kk: ['автобус аялдамасы'] }),
8
- group('Clinic', { ru: ['поликлиника', 'клиника'], en: ['clinic', 'polyclinic'], uk: ['поліклініка', 'клініка'], ro: ['clinică', 'clinica', 'policlinică', 'policlinica'], uzLatn: ['poliklinika', 'poleklinika', 'klinika'], uzCyrl: ['поликлиника', 'клиника'], kk: ['емхана', 'клиника'] }),
9
- group('Hospital', { ru: ['больница', 'госпиталь'], en: ['hospital'], uk: ['лікарня', 'госпіталь'], ro: ['spital'], uzLatn: ['shifoxona', 'kasalxona'], uzCyrl: ['шифохона', 'касалхона'], kk: ['аурухана'] }),
10
- group('School', { ru: ['школа', 'школы', 'школу', 'школе'], en: ['school'], uk: ['школа', 'школи', 'школу'], ro: ['școală', 'scoala'], uzLatn: ['maktab'], uzCyrl: ['мактаб'], kk: ['мектеп'] }),
11
- group('Kindergarten', { ru: ['детский сад', 'садик'], en: ['kindergarten', 'nursery'], uk: ['дитячий садок', 'садочок'], ro: ['grădiniță', 'gradinita'], uzLatn: ["bolalar bog'chasi", 'bolalar bog‘chasi', 'bogcha'], uzCyrl: ['болалар боғчаси', 'боғча'], kk: ['балабақша'] }),
12
- group('University', { ru: ['университет', 'институт'], en: ['university', 'institute'], uk: ['університет', 'інститут'], ro: ['universitate', 'institut'], uzLatn: ['universitet', 'institut'], uzCyrl: ['университет', 'институт'], kk: ['университет', 'институт'] }),
13
- group('Shopping center', { ru: ['тц', 'трц', 'торговый центр'], en: ['shopping center', 'shopping centre', 'mall'], uk: ['тц', 'трц', 'торговий центр'], ro: ['centru comercial', 'mall'], uzLatn: ['savdo markazi', 'mall'], uzCyrl: ['савдо маркази'], kk: ['сауда орталығы', 'сауда үйі'] }),
14
- group('Supermarket', { ru: ['супермаркет', 'гипермаркет'], en: ['supermarket', 'hypermarket'], uk: ['супермаркет', 'гіпермаркет'], ro: ['supermarket', 'hipermarket'], uzLatn: ['supermarket', 'gipermarket'], uzCyrl: ['супермаркет', 'гипермаркет'], kk: ['супермаркет', 'гипермаркет'] }),
15
- group('Market', { ru: ['рынок', 'базар'], en: ['market', 'bazaar'], uk: ['ринок', 'базар'], ro: ['piață', 'piata', 'bazar'], uzLatn: ['bozor', 'bazar'], uzCyrl: ['бозор', 'базар'], kk: ['базар', 'нарық'] }),
16
- group('Pharmacy', { ru: ['аптека'], en: ['pharmacy', 'drugstore'], uk: ['аптека'], ro: ['farmacie'], uzLatn: ['dorixona', 'apteka'], uzCyrl: ['дорихона', 'аптека'], kk: ['дәріхана', 'аптека'] }),
17
- group('Mosque', { ru: ['мечеть'], en: ['mosque'], uk: ['мечеть'], ro: ['moschee'], uzLatn: ['masjid'], uzCyrl: ['масжид'], kk: ['мешіт'] }),
18
- group('Church', { ru: ['церковь', 'храм'], en: ['church', 'cathedral'], uk: ['церква', 'храм', 'собор'], ro: ['biserică', 'biserica', 'catedrală', 'catedrala'], uzLatn: ['cherkov', 'ibodatxona'], uzCyrl: ['черков'], kk: ['шіркеу', 'собор'] }),
19
- group('Railway station', { ru: ['вокзал', 'железнодорожный вокзал'], en: ['railway station', 'train station'], uk: ['вокзал', 'залізничний вокзал'], ro: ['gară', 'gara', 'gară feroviară', 'gara feroviara'], uzLatn: ['vokzal', 'temir yol vokzali', "temir yo'l vokzali"], uzCyrl: ['вокзал', 'темир йўл вокзали'], kk: ['теміржол вокзалы', 'вокзал'] }),
20
- group('Airport', { ru: ['аэропорт'], en: ['airport'], uk: ['аеропорт'], ro: ['aeroport'], uzLatn: ['aeroport'], uzCyrl: ['аэропорт'], kk: ['әуежай', 'аэропорт'] }),
21
- ]);
1
+ import { lexiconEntity } from './lexicon-core.js';
2
+ const group = (canonical, aliases) => lexiconEntity(canonical, aliases);
3
+
4
+ export const GENERIC_LANDMARK_TERMS = Object.freeze([
5
+ group('Park', { ru: ['парк'], en: ['park'], uk: ['парк'], ro: ['parc'], uzLatn: ["bog'", 'bog‘', 'bogʻ', 'park'], uzCyrl: ['боғ', 'парк'], kk: ['саябақ', 'парк'] }),
6
+ group('Metro', { ru: ['метро', 'станция метро'], en: ['metro', 'subway station'], uk: ['метро', 'станція метро'], ro: ['metrou', 'stație de metrou'], uzLatn: ['metro', 'metro bekati'], uzCyrl: ['метро', 'метро бекати'], kk: ['метро', 'метро станциясы'] }),
7
+ group('Bus stop', { ru: ['автобусная остановка', 'остановка автобуса', 'конечная автобуса'], en: ['bus stop', 'bus station'], uk: ['автобусна зупинка', 'зупинка автобуса'], ro: ['stație de autobuz', 'statie de autobuz'], uzLatn: ['avtobus bekati', 'avtobus kanichka', 'avtobus kanichkasi', 'avtobus konichka', 'avtobus konichkasi'], uzCyrl: ['автобус бекати'], kk: ['автобус аялдамасы'] }),
8
+ group('Clinic', { ru: ['поликлиника', 'клиника'], en: ['clinic', 'polyclinic'], uk: ['поліклініка', 'клініка'], ro: ['clinică', 'clinica', 'policlinică', 'policlinica'], uzLatn: ['poliklinika', 'poleklinika', 'klinika'], uzCyrl: ['поликлиника', 'клиника'], kk: ['емхана', 'клиника'] }),
9
+ group('Hospital', { ru: ['больница', 'госпиталь'], en: ['hospital'], uk: ['лікарня', 'госпіталь'], ro: ['spital'], uzLatn: ['shifoxona', 'kasalxona'], uzCyrl: ['шифохона', 'касалхона'], kk: ['аурухана'] }),
10
+ group('School', { ru: ['школа', 'школы', 'школу', 'школе'], en: ['school'], uk: ['школа', 'школи', 'школу'], ro: ['școală', 'scoala'], uzLatn: ['maktab'], uzCyrl: ['мактаб'], kk: ['мектеп'] }),
11
+ group('Kindergarten', { ru: ['детский сад', 'садик'], en: ['kindergarten', 'nursery'], uk: ['дитячий садок', 'садочок'], ro: ['grădiniță', 'gradinita'], uzLatn: ["bolalar bog'chasi", 'bolalar bog‘chasi', 'bogcha'], uzCyrl: ['болалар боғчаси', 'боғча'], kk: ['балабақша'] }),
12
+ group('University', { ru: ['университет', 'институт'], en: ['university', 'institute'], uk: ['університет', 'інститут'], ro: ['universitate', 'institut'], uzLatn: ['universitet', 'institut'], uzCyrl: ['университет', 'институт'], kk: ['университет', 'институт'] }),
13
+ group('Shopping center', { ru: ['тц', 'трц', 'торговый центр'], en: ['shopping center', 'shopping centre', 'mall'], uk: ['тц', 'трц', 'торговий центр'], ro: ['centru comercial', 'mall'], uzLatn: ['savdo markazi', 'mall'], uzCyrl: ['савдо маркази'], kk: ['сауда орталығы', 'сауда үйі'] }),
14
+ group('Supermarket', { ru: ['супермаркет', 'гипермаркет'], en: ['supermarket', 'hypermarket'], uk: ['супермаркет', 'гіпермаркет'], ro: ['supermarket', 'hipermarket'], uzLatn: ['supermarket', 'gipermarket'], uzCyrl: ['супермаркет', 'гипермаркет'], kk: ['супермаркет', 'гипермаркет'] }),
15
+ group('Market', { ru: ['рынок', 'базар'], en: ['market', 'bazaar'], uk: ['ринок', 'базар'], ro: ['piață', 'piata', 'bazar'], uzLatn: ['bozor', 'bazar'], uzCyrl: ['бозор', 'базар'], kk: ['базар', 'нарық'] }),
16
+ group('Pharmacy', { ru: ['аптека'], en: ['pharmacy', 'drugstore'], uk: ['аптека'], ro: ['farmacie'], uzLatn: ['dorixona', 'apteka'], uzCyrl: ['дорихона', 'аптека'], kk: ['дәріхана', 'аптека'] }),
17
+ group('Mosque', { ru: ['мечеть'], en: ['mosque'], uk: ['мечеть'], ro: ['moschee'], uzLatn: ['masjid'], uzCyrl: ['масжид'], kk: ['мешіт'] }),
18
+ group('Church', { ru: ['церковь', 'храм'], en: ['church', 'cathedral'], uk: ['церква', 'храм', 'собор'], ro: ['biserică', 'biserica', 'catedrală', 'catedrala'], uzLatn: ['cherkov', 'ibodatxona'], uzCyrl: ['черков'], kk: ['шіркеу', 'собор'] }),
19
+ group('Railway station', { ru: ['вокзал', 'железнодорожный вокзал'], en: ['railway station', 'train station'], uk: ['вокзал', 'залізничний вокзал'], ro: ['gară', 'gara', 'gară feroviară', 'gara feroviara'], uzLatn: ['vokzal', 'temir yol vokzali', "temir yo'l vokzali"], uzCyrl: ['вокзал', 'темир йўл вокзали'], kk: ['теміржол вокзалы', 'вокзал'] }),
20
+ group('Airport', { ru: ['аэропорт'], en: ['airport'], uk: ['аеропорт'], ro: ['aeroport'], uzLatn: ['aeroport'], uzCyrl: ['аэропорт'], kk: ['әуежай', 'аэропорт'] }),
21
+ ]);
@@ -1,58 +1,58 @@
1
- export const LEXICON_LANGUAGES = Object.freeze([
2
- 'all',
3
- 'ru',
4
- 'en',
5
- 'uk',
6
- 'ro',
7
- 'uzLatn',
8
- 'uzCyrl',
9
- 'kaaLat',
10
- 'kaaCyrl',
11
- 'kk',
12
- ]);
13
-
14
- export const COUNTRY_CODES = Object.freeze(['UZ', 'KZ', 'UA', 'RO', 'KG']);
15
-
16
- export function deepFreeze(value, seen = new WeakSet()) {
17
- if (value == null || (typeof value !== 'object' && typeof value !== 'function')) return value;
18
- if (seen.has(value)) return value;
19
- seen.add(value);
20
-
21
- // Freezing Map/Set objects does not make their entries immutable, so they are
22
- // intentionally not used as public mutable lexicon indexes. Freeze ordinary
23
- // nested objects/arrays/regexes only.
24
- if (value instanceof Map || value instanceof Set) return value;
25
-
26
- for (const key of Reflect.ownKeys(value)) {
27
- const child = value[key];
28
- if (child && (typeof child === 'object' || typeof child === 'function')) deepFreeze(child, seen);
29
- }
30
- return Object.freeze(value);
31
- }
32
-
33
- export function freezeAliases(aliases = {}) {
34
- if (Array.isArray(aliases)) return Object.freeze([...aliases]);
35
- return Object.freeze(Object.fromEntries(
36
- Object.entries(aliases).map(([language, values]) => [
37
- language,
38
- Object.freeze(Array.isArray(values) ? [...values] : []),
39
- ]),
40
- ));
41
- }
42
-
43
- export function lexiconEntity(canonical, aliases = {}, extra = {}) {
44
- return deepFreeze({
45
- ...extra,
46
- canonical,
47
- aliases: freezeAliases(aliases),
48
- });
49
- }
50
-
51
- export function locationEntity(canonical, aliases = [], extra = {}) {
52
- return deepFreeze({
53
- ...extra,
54
- canonical,
55
- name: canonical,
56
- aliases: Object.freeze([...aliases]),
57
- });
58
- }
1
+ export const LEXICON_LANGUAGES = Object.freeze([
2
+ 'all',
3
+ 'ru',
4
+ 'en',
5
+ 'uk',
6
+ 'ro',
7
+ 'uzLatn',
8
+ 'uzCyrl',
9
+ 'kaaLat',
10
+ 'kaaCyrl',
11
+ 'kk',
12
+ ]);
13
+
14
+ export const COUNTRY_CODES = Object.freeze(['UZ', 'KZ', 'UA', 'RO', 'KG']);
15
+
16
+ export function deepFreeze(value, seen = new WeakSet()) {
17
+ if (value == null || (typeof value !== 'object' && typeof value !== 'function')) return value;
18
+ if (seen.has(value)) return value;
19
+ seen.add(value);
20
+
21
+ // Freezing Map/Set objects does not make their entries immutable, so they are
22
+ // intentionally not used as public mutable lexicon indexes. Freeze ordinary
23
+ // nested objects/arrays/regexes only.
24
+ if (value instanceof Map || value instanceof Set) return value;
25
+
26
+ for (const key of Reflect.ownKeys(value)) {
27
+ const child = value[key];
28
+ if (child && (typeof child === 'object' || typeof child === 'function')) deepFreeze(child, seen);
29
+ }
30
+ return Object.freeze(value);
31
+ }
32
+
33
+ export function freezeAliases(aliases = {}) {
34
+ if (Array.isArray(aliases)) return Object.freeze([...aliases]);
35
+ return Object.freeze(Object.fromEntries(
36
+ Object.entries(aliases).map(([language, values]) => [
37
+ language,
38
+ Object.freeze(Array.isArray(values) ? [...values] : []),
39
+ ]),
40
+ ));
41
+ }
42
+
43
+ export function lexiconEntity(canonical, aliases = {}, extra = {}) {
44
+ return deepFreeze({
45
+ ...extra,
46
+ canonical,
47
+ aliases: freezeAliases(aliases),
48
+ });
49
+ }
50
+
51
+ export function locationEntity(canonical, aliases = [], extra = {}) {
52
+ return deepFreeze({
53
+ ...extra,
54
+ canonical,
55
+ name: canonical,
56
+ aliases: Object.freeze([...aliases]),
57
+ });
58
+ }
@@ -1,106 +1,106 @@
1
- import { aliasesToRegex, normalizeForMatch } from './normalization.js';
2
-
3
- export const LOCATION_LIST_KEYS = Object.freeze([
4
- 'districts',
5
- 'microdistricts',
6
- 'mahallas',
7
- 'localAreas',
8
- 'suburbs',
9
- 'settlements',
10
- 'metro',
11
- 'residentialComplexes',
12
- 'streets',
13
- 'landmarks',
14
- 'pois',
15
- 'searchClusters',
16
- ]);
17
-
18
- export function locationEntry(name, ...aliases) {
19
- const all = [...new Set([name, ...aliases].flat().filter(Boolean))];
20
- return Object.freeze({ canonical: name, name, aliases: Object.freeze(all), re: aliasesToRegex(all) });
21
- }
22
-
23
- export function locationEntries(rows = []) {
24
- return Object.freeze(rows.map(([name, ...aliases]) => locationEntry(name, ...aliases)));
25
- }
26
-
27
- function mergeEntry(existing, incoming) {
28
- const aliases = [...new Set([
29
- ...(existing?.aliases || []),
30
- ...(incoming?.aliases || []),
31
- existing?.name,
32
- incoming?.name,
33
- ].filter(Boolean))];
34
- const base = { ...(existing || {}), ...(incoming || {}) };
35
- return Object.freeze({ ...base, canonical: base.canonical || base.name, type: base.type || base.entityType, aliases: Object.freeze(aliases), re: aliasesToRegex(aliases) });
36
- }
37
-
38
- function parentKey(entry) {
39
- return normalizeForMatch(entry?.parent || entry?.district || '');
40
- }
41
-
42
- export function mergeLocationEntries(...lists) {
43
- const groups = new Map();
44
- const order = [];
45
-
46
- for (const list of lists) {
47
- for (const entry of list || []) {
48
- if (!entry?.name) continue;
49
- const canonical = normalizeForMatch(entry.name);
50
- if (!groups.has(canonical)) {
51
- groups.set(canonical, []);
52
- order.push(canonical);
53
- }
54
- groups.get(canonical).push(entry);
55
- }
56
- }
57
-
58
- const result = [];
59
- for (const canonical of order) {
60
- const group = groups.get(canonical) || [];
61
- const scopedParents = [...new Set(group.map(parentKey).filter(Boolean))];
62
-
63
- if (scopedParents.length <= 1) {
64
- result.push(group.reduce((merged, entry) => mergeEntry(merged, entry), null));
65
- continue;
66
- }
67
-
68
- // A market name can legitimately exist under multiple parents inside one
69
- // city (for example Sairan in adjacent Almaty districts). Keep each scoped
70
- // entity. Unscoped/base aliases are merged into every scoped variant so
71
- // older dictionaries enrich rather than erase parent information.
72
- const unscoped = group.filter((entry) => !parentKey(entry));
73
- for (const parent of scopedParents) {
74
- const scoped = group.filter((entry) => parentKey(entry) === parent);
75
- result.push([...unscoped, ...scoped].reduce((merged, entry) => mergeEntry(merged, entry), null));
76
- }
77
- }
78
-
79
- return Object.freeze(result);
80
- }
81
-
82
- export function mergeLocationCityDictionaries(...dictionaries) {
83
- const result = {};
84
- for (const key of LOCATION_LIST_KEYS) {
85
- const lists = dictionaries.map((dictionary) => dictionary?.[key]).filter(Boolean);
86
- if (lists.length) result[key] = mergeLocationEntries(...lists);
87
- }
88
-
89
- for (const dictionary of dictionaries) {
90
- if (!dictionary) continue;
91
- for (const [key, value] of Object.entries(dictionary)) {
92
- if (LOCATION_LIST_KEYS.includes(key) || value == null) continue;
93
- result[key] = value;
94
- }
95
- }
96
-
97
- return Object.freeze(result);
98
- }
99
-
100
- export function mergeLocationCountries(...countries) {
101
- const cityNames = [...new Set(countries.flatMap((country) => Object.keys(country || {})))];
102
- return Object.freeze(Object.fromEntries(cityNames.map((city) => [
103
- city,
104
- mergeLocationCityDictionaries(...countries.map((country) => country?.[city]).filter(Boolean)),
105
- ])));
106
- }
1
+ import { aliasesToRegex, normalizeForMatch } from './normalization.js';
2
+
3
+ export const LOCATION_LIST_KEYS = Object.freeze([
4
+ 'districts',
5
+ 'microdistricts',
6
+ 'mahallas',
7
+ 'localAreas',
8
+ 'suburbs',
9
+ 'settlements',
10
+ 'metro',
11
+ 'residentialComplexes',
12
+ 'streets',
13
+ 'landmarks',
14
+ 'pois',
15
+ 'searchClusters',
16
+ ]);
17
+
18
+ export function locationEntry(name, ...aliases) {
19
+ const all = [...new Set([name, ...aliases].flat().filter(Boolean))];
20
+ return Object.freeze({ canonical: name, name, aliases: Object.freeze(all), re: aliasesToRegex(all) });
21
+ }
22
+
23
+ export function locationEntries(rows = []) {
24
+ return Object.freeze(rows.map(([name, ...aliases]) => locationEntry(name, ...aliases)));
25
+ }
26
+
27
+ function mergeEntry(existing, incoming) {
28
+ const aliases = [...new Set([
29
+ ...(existing?.aliases || []),
30
+ ...(incoming?.aliases || []),
31
+ existing?.name,
32
+ incoming?.name,
33
+ ].filter(Boolean))];
34
+ const base = { ...(existing || {}), ...(incoming || {}) };
35
+ return Object.freeze({ ...base, canonical: base.canonical || base.name, type: base.type || base.entityType, aliases: Object.freeze(aliases), re: aliasesToRegex(aliases) });
36
+ }
37
+
38
+ function parentKey(entry) {
39
+ return normalizeForMatch(entry?.parent || entry?.district || '');
40
+ }
41
+
42
+ export function mergeLocationEntries(...lists) {
43
+ const groups = new Map();
44
+ const order = [];
45
+
46
+ for (const list of lists) {
47
+ for (const entry of list || []) {
48
+ if (!entry?.name) continue;
49
+ const canonical = normalizeForMatch(entry.name);
50
+ if (!groups.has(canonical)) {
51
+ groups.set(canonical, []);
52
+ order.push(canonical);
53
+ }
54
+ groups.get(canonical).push(entry);
55
+ }
56
+ }
57
+
58
+ const result = [];
59
+ for (const canonical of order) {
60
+ const group = groups.get(canonical) || [];
61
+ const scopedParents = [...new Set(group.map(parentKey).filter(Boolean))];
62
+
63
+ if (scopedParents.length <= 1) {
64
+ result.push(group.reduce((merged, entry) => mergeEntry(merged, entry), null));
65
+ continue;
66
+ }
67
+
68
+ // A market name can legitimately exist under multiple parents inside one
69
+ // city (for example Sairan in adjacent Almaty districts). Keep each scoped
70
+ // entity. Unscoped/base aliases are merged into every scoped variant so
71
+ // older dictionaries enrich rather than erase parent information.
72
+ const unscoped = group.filter((entry) => !parentKey(entry));
73
+ for (const parent of scopedParents) {
74
+ const scoped = group.filter((entry) => parentKey(entry) === parent);
75
+ result.push([...unscoped, ...scoped].reduce((merged, entry) => mergeEntry(merged, entry), null));
76
+ }
77
+ }
78
+
79
+ return Object.freeze(result);
80
+ }
81
+
82
+ export function mergeLocationCityDictionaries(...dictionaries) {
83
+ const result = {};
84
+ for (const key of LOCATION_LIST_KEYS) {
85
+ const lists = dictionaries.map((dictionary) => dictionary?.[key]).filter(Boolean);
86
+ if (lists.length) result[key] = mergeLocationEntries(...lists);
87
+ }
88
+
89
+ for (const dictionary of dictionaries) {
90
+ if (!dictionary) continue;
91
+ for (const [key, value] of Object.entries(dictionary)) {
92
+ if (LOCATION_LIST_KEYS.includes(key) || value == null) continue;
93
+ result[key] = value;
94
+ }
95
+ }
96
+
97
+ return Object.freeze(result);
98
+ }
99
+
100
+ export function mergeLocationCountries(...countries) {
101
+ const cityNames = [...new Set(countries.flatMap((country) => Object.keys(country || {})))];
102
+ return Object.freeze(Object.fromEntries(cityNames.map((city) => [
103
+ city,
104
+ mergeLocationCityDictionaries(...countries.map((country) => country?.[city]).filter(Boolean)),
105
+ ])));
106
+ }