@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
@@ -1,64 +1,64 @@
1
- import { COUNTRIES, canonicalCountryCode } from './countries.js';
2
- import { CITIES } from './geography.js';
3
- import { aliasesOf, aliasesToRegex, normalizeForMatch } from './normalization.js';
4
-
5
- const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6
-
7
- function cityInflectionRegex(aliases) {
8
- const cyrillic = [...new Set(aliases.filter((alias) => /\p{Script=Cyrillic}/u.test(alias)))];
9
- if (!cyrillic.length) return null;
10
- const patterns = cyrillic.map((alias) => {
11
- const maxSuffix = alias.replace(/[^\p{L}\p{N}]/gu, '').length > 3 ? 5 : 3;
12
- return `${escapeRegex(alias)}(?:\\p{Script=Cyrillic}{0,${maxSuffix}})`;
13
- });
14
- return new RegExp(`(?<![\\p{L}\\p{N}])(?:${patterns.join('|')})(?![\\p{L}\\p{N}])`, 'iu');
15
- }
16
-
17
- const CITY_MATCHERS = CITIES.map((item) => {
18
- const aliases = [item.canonical, ...aliasesOf(item)];
19
- return Object.freeze({
20
- item,
21
- exact: aliasesToRegex(aliases),
22
- inflected: cityInflectionRegex(aliases),
23
- });
24
- });
25
-
26
- const COUNTRY_MATCHERS = COUNTRIES.map((item) => {
27
- // Two-letter country codes are intentionally excluded from arbitrary prose:
28
- // e.g. English "us" must not make a vacancy a US role. Exact ISO-2 values
29
- // are still handled by canonicalCountryCode().
30
- const aliases = [item.canonical, ...aliasesOf(item)]
31
- .filter((alias) => normalizeForMatch(alias).replace(/\s/g, '').length > 2);
32
- return Object.freeze({ item, re: aliasesToRegex(aliases) });
33
- });
34
-
35
- 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/;
36
-
37
- /** Detect an ISO-2 country from a country alias, known city alias, or US city/state location. */
38
- export function detectCountryCodeFromText(value) {
39
- const text = String(value || '').trim();
40
- if (!text) return null;
41
-
42
- const exact = canonicalCountryCode(text);
43
- if (exact) return exact;
44
-
45
- const country = COUNTRY_MATCHERS.find(({ re }) => re.test(text))?.item;
46
- if (country?.code) return country.code;
47
-
48
- // Keep dotted U.S. and explicit "remote US" support without treating the
49
- // ordinary English pronoun "us" as a geography signal.
50
- if (/\bU\.S\.?\b/.test(text) || /\bremote\s+us\b/i.test(text) || US_STATE_RE.test(text)) return 'US';
51
-
52
- return detectCityFromText(text)?.country || null;
53
- }
54
-
55
- /** Detect a known city in free text, including common Cyrillic case inflections. */
56
- export function detectCityFromText(value, country = null) {
57
- const text = String(value || '');
58
- if (!text) return null;
59
- const code = country ? canonicalCountryCode(country) : null;
60
- const match = CITY_MATCHERS.find(({ item, exact, inflected }) =>
61
- (!code || item.country === code) && (exact.test(text) || Boolean(inflected?.test(text))));
62
- if (!match) return null;
63
- return Object.freeze({ canonical: match.item.canonical, country: match.item.country || null });
64
- }
1
+ import { COUNTRIES, canonicalCountryCode } from './countries.js';
2
+ import { CITIES } from './geography.js';
3
+ import { aliasesOf, aliasesToRegex, normalizeForMatch } from './normalization.js';
4
+
5
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6
+
7
+ function cityInflectionRegex(aliases) {
8
+ const cyrillic = [...new Set(aliases.filter((alias) => /\p{Script=Cyrillic}/u.test(alias)))];
9
+ if (!cyrillic.length) return null;
10
+ const patterns = cyrillic.map((alias) => {
11
+ const maxSuffix = alias.replace(/[^\p{L}\p{N}]/gu, '').length > 3 ? 5 : 3;
12
+ return `${escapeRegex(alias)}(?:\\p{Script=Cyrillic}{0,${maxSuffix}})`;
13
+ });
14
+ return new RegExp(`(?<![\\p{L}\\p{N}])(?:${patterns.join('|')})(?![\\p{L}\\p{N}])`, 'iu');
15
+ }
16
+
17
+ const CITY_MATCHERS = CITIES.map((item) => {
18
+ const aliases = [item.canonical, ...aliasesOf(item)];
19
+ return Object.freeze({
20
+ item,
21
+ exact: aliasesToRegex(aliases),
22
+ inflected: cityInflectionRegex(aliases),
23
+ });
24
+ });
25
+
26
+ const COUNTRY_MATCHERS = COUNTRIES.map((item) => {
27
+ // Two-letter country codes are intentionally excluded from arbitrary prose:
28
+ // e.g. English "us" must not make a vacancy a US role. Exact ISO-2 values
29
+ // are still handled by canonicalCountryCode().
30
+ const aliases = [item.canonical, ...aliasesOf(item)]
31
+ .filter((alias) => normalizeForMatch(alias).replace(/\s/g, '').length > 2);
32
+ return Object.freeze({ item, re: aliasesToRegex(aliases) });
33
+ });
34
+
35
+ 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/;
36
+
37
+ /** Detect an ISO-2 country from a country alias, known city alias, or US city/state location. */
38
+ export function detectCountryCodeFromText(value) {
39
+ const text = String(value || '').trim();
40
+ if (!text) return null;
41
+
42
+ const exact = canonicalCountryCode(text);
43
+ if (exact) return exact;
44
+
45
+ const country = COUNTRY_MATCHERS.find(({ re }) => re.test(text))?.item;
46
+ if (country?.code) return country.code;
47
+
48
+ // Keep dotted U.S. and explicit "remote US" support without treating the
49
+ // ordinary English pronoun "us" as a geography signal.
50
+ if (/\bU\.S\.?\b/.test(text) || /\bremote\s+us\b/i.test(text) || US_STATE_RE.test(text)) return 'US';
51
+
52
+ return detectCityFromText(text)?.country || null;
53
+ }
54
+
55
+ /** Detect a known city in free text, including common Cyrillic case inflections. */
56
+ export function detectCityFromText(value, country = null) {
57
+ const text = String(value || '');
58
+ if (!text) return null;
59
+ const code = country ? canonicalCountryCode(country) : null;
60
+ const match = CITY_MATCHERS.find(({ item, exact, inflected }) =>
61
+ (!code || item.country === code) && (exact.test(text) || Boolean(inflected?.test(text))));
62
+ if (!match) return null;
63
+ return Object.freeze({ canonical: match.item.canonical, country: match.item.country || null });
64
+ }
@@ -1,13 +1,13 @@
1
- export type GeographyDisplayKind = 'country' | 'region' | 'city' | 'district' | 'metro' | 'any';
2
-
3
- export const GEOGRAPHY_DISPLAY_NAMES: Readonly<Record<string, Readonly<{
4
- country: Readonly<Record<string, string>>;
5
- city: Readonly<Record<string, string>>;
6
- region?: Readonly<Record<string, string>>;
7
- district: Readonly<Record<string, string>>;
8
- metro: Readonly<Record<string, string>>;
9
- metroAlias: Readonly<Record<string, string>>;
10
- }>>>;
11
-
12
- export function geographyDisplayName(value: unknown, locale?: string, kind?: GeographyDisplayKind): string;
13
- export function geographyMetroLabelWithAlias(value: unknown, locale?: string): string;
1
+ export type GeographyDisplayKind = 'country' | 'region' | 'city' | 'district' | 'metro' | 'any';
2
+
3
+ export const GEOGRAPHY_DISPLAY_NAMES: Readonly<Record<string, Readonly<{
4
+ country: Readonly<Record<string, string>>;
5
+ city: Readonly<Record<string, string>>;
6
+ region?: Readonly<Record<string, string>>;
7
+ district: Readonly<Record<string, string>>;
8
+ metro: Readonly<Record<string, string>>;
9
+ metroAlias: Readonly<Record<string, string>>;
10
+ }>>>;
11
+
12
+ export function geographyDisplayName(value: unknown, locale?: string, kind?: GeographyDisplayKind): string;
13
+ export function geographyMetroLabelWithAlias(value: unknown, locale?: string): string;
@@ -1,113 +1,113 @@
1
- import { COUNTRIES } from './countries.js';
2
- import { CITIES, REGIONS } from './geography.js';
3
-
4
- // Centralized presentation names for canonical geography values.
5
- // Consumers must not maintain their own country/city/district/metro display dictionaries.
6
-
7
- export const GEOGRAPHY_DISPLAY_NAMES = Object.freeze({
8
- ru: Object.freeze({
9
- country: Object.freeze({ UZ: 'Узбекистан', KZ: 'Казахстан', UA: 'Украина', RO: 'Румыния', KG: 'Кыргызстан' }),
10
- city: Object.freeze({
11
- Tashkent: 'Ташкент', Samarkand: 'Самарканд', Bukhara: 'Бухара', Namangan: 'Наманган',
12
- Andijan: 'Андижан', Fergana: 'Фергана', Nukus: 'Нукус', Navoi: 'Навои', Navoiy: 'Навои', Jizzakh: 'Джизак',
13
- Termez: 'Термез', Qarshi: 'Карши', Urgench: 'Ургенч', Gulistan: 'Гулистан', Chirchiq: 'Чирчик',
14
- 'Tashkent Region': 'Ташкентская область', Karakalpakstan: 'Каракалпакстан', Kashkadarya: 'Кашкадарья',
15
- Surkhandarya: 'Сурхандарья', Syrdarya: 'Сырдарья', Khorezm: 'Хорезм',
16
- Almaty: 'Алматы', Astana: 'Астана', Shymkent: 'Шымкент', Karaganda: 'Караганда',
17
- Aktobe: 'Актобе', Atyrau: 'Атырау', Oral: 'Уральск', Taraz: 'Тараз', Pavlodar: 'Павлодар',
18
- Semey: 'Семей', Kostanay: 'Костанай', Kyzylorda: 'Кызылорда', Aktau: 'Актау', Oskemen: 'Усть-Каменогорск',
19
- Kyiv: 'Киев', Lviv: 'Львов', Odesa: 'Одесса', Kharkiv: 'Харьков', Dnipro: 'Днепр',
20
- Vinnytsia: 'Винница', 'Ivano-Frankivsk': 'Ивано-Франковск', Lutsk: 'Луцк', Chernivtsi: 'Черновцы',
21
- Zaporizhzhia: 'Запорожье', Poltava: 'Полтава', Rivne: 'Ровно', Ternopil: 'Тернополь',
22
- Uzhhorod: 'Ужгород', Khmelnytskyi: 'Хмельницкий', Zhytomyr: 'Житомир', Cherkasy: 'Черкассы',
23
- Chernihiv: 'Чернигов', Sumy: 'Сумы', Mykolaiv: 'Николаев', Kropyvnytskyi: 'Кропивницкий',
24
- Kherson: 'Херсон', 'Kryvyi Rih': 'Кривой Рог', Kremenchuk: 'Кременчуг', 'Bila Tserkva': 'Белая Церковь',
25
- Kamianske: 'Каменское', Vyshneve: 'Вишневое', Boryspil: 'Борисполь', Vyshhorod: 'Вышгород',
26
- Oleksandriia: 'Александрия', Pavlohrad: 'Павлоград', Nikopol: 'Никополь', Drohobych: 'Дрогобыч',
27
- Stryi: 'Стрый', Kolomyia: 'Коломыя', Kalush: 'Калуш', 'Kamianets-Podilskyi': 'Каменец-Подольский',
28
- Izmail: 'Измаил',
29
- Bucharest: 'Бухарест', 'Cluj-Napoca': 'Клуж-Напока', Timisoara: 'Тимишоара', Iasi: 'Яссы',
30
- Brasov: 'Брашов', Constanta: 'Констанца', Oradea: 'Орадя', Sibiu: 'Сибиу',
31
- }),
32
- district: Object.freeze({
33
- Chilanzar: 'Чиланзар', Yunusabad: 'Юнусабад', 'Mirzo Ulugbek': 'Мирзо-Улугбек',
34
- Yakkasaray: 'Яккасарай', Shaykhantahur: 'Шайхантахур', Yashnobod: 'Яшнабад', Sergeli: 'Сергели',
35
- Uchtepa: 'Учтепа', Mirobod: 'Мирабад', Bektemir: 'Бектемир', Olmazor: 'Алмазар',
36
- Almaly: 'Алмалинский', Bostandyk: 'Бостандыкский', Medeu: 'Медеуский', Auezov: 'Ауэзовский',
37
- Turksib: 'Турксибский', Nauryzbay: 'Наурызбайский', Alatau: 'Алатауский', Zhetysu: 'Жетысуский',
38
- Podil: 'Подол', Pechersk: 'Печерск', Pecherskyi: 'Печерский', Obolon: 'Оболонь',
39
- Shevchenkivskyi: 'Шевченковский', Solomianskyi: 'Соломенский', Darnytskyi: 'Дарницкий',
40
- Holosiivskyi: 'Голосеевский', Dniprovskyi: 'Днепровский', Sviatoshynskyi: 'Святошинский',
41
- Desnianskyi: 'Деснянский', Prymorskyi: 'Приморский', Frankivskyi: 'Франковский',
42
- Tsentralnyi: 'Центральный', 'Tsentralno-Miskyi': 'Центрально-Городской', Tsentr: 'Центр',
43
- Prospekt: 'Проспект', Ruska: 'Русская', Hraviton: 'Гравитон', Komarova: 'Комарова', Roscha: 'Роща',
44
- Sadgora: 'Садгора', Avtovokzal: 'Автовокзал', Pipera: 'Пипера', Militari: 'Милитари',
45
- 'Drumul Taberei': 'Друмул Таберей', Titan: 'Титан', Berceni: 'Берчень', Floreasca: 'Флоряска',
46
- Dorobanti: 'Доробанць', Cotroceni: 'Котрочень',
47
- }),
48
- metro: Object.freeze({
49
- 'Buyuk Ipak Yoli': 'Буюк Ипак Йули', Pushkin: 'Пушкин', 'Hamid Olimjon': 'Хамид Алимджан',
50
- 'Amir Temur Xiyoboni': 'Амир Темур Хиёбони', 'Mustaqillik Maydoni': 'Мустакиллик майдони',
51
- Paxtakor: 'Пахтакор', 'Xalqlar Dostligi': 'Халклар Дустлиги', 'Milliy Bog': 'Миллий Бог', Novza: 'Новза',
52
- 'Mirzo Ulugbek': 'Мирзо Улугбек', Chilonzor: 'Чиланзар', Olmazor: 'Алмазар', Choshtepa: 'Чаштепа',
53
- Ozgarish: 'Узгариш', Sergeli: 'Сергели', Yangihayot: 'Янгихаёт', Chinor: 'Чинар', Beruniy: 'Беруни',
54
- Tinchlik: 'Тинчлик', Chorsu: 'Чорсу', 'Gafur Gulom': 'Гафур Гулям', 'Alisher Navoi': 'Алишер Навои',
55
- Ozbekiston: 'Узбекистан', Kosmonavtlar: 'Космонавтлар', Oybek: 'Ойбек', Toshkent: 'Ташкент',
56
- Mashinasozlar: 'Машинасозлар', Dostlik: 'Дустлик', Turkiston: 'Туркистон', Yunusobod: 'Юнусабад',
57
- Shahriston: 'Шахристан', Bodomzor: 'Бадамзар', Minor: 'Минор', 'Abdulla Qodiriy': 'Абдулла Кадыри',
58
- 'Yunus Rajabiy': 'Юнус Раджаби', 'Ming Orik': 'Мингурик', Texnopark: 'Технопарк', Yashnobod: 'Яшнабад',
59
- Tuzel: 'Тузель', Olmos: 'Алмас', Rohat: 'Рохат', Yangiobod: 'Янгиабад', Qoyliq: 'Куйлюк', Matonat: 'Матонат',
60
- Qiyot: 'Кият', Tolariq: 'Толарык', Xonobod: 'Хонабад', Quruvchilar: 'Курувчилар', Turon: 'Туран', Qipchoq: 'Кипчак',
61
- }),
62
- metroAlias: Object.freeze({
63
- 'Buyuk Ipak Yoli': 'Максим Горький', Novza: 'Хамза',
64
- 'Amir Temur Xiyoboni': 'Сквер Октябрьской Революции', 'Mustaqillik Maydoni': 'Площадь Ленина',
65
- 'Milliy Bog': 'Национальный парк', Yunusobod: 'Фахрийлар чойхонаси',
66
- 'Xalqlar Dostligi': 'Дружба народов', Ozbekiston: 'Узбекистан', Mashinasozlar: 'Машиностроителей',
67
- Dostlik: 'Дружба', Tinchlik: 'Мир',
68
- }),
69
- }),
70
- });
71
-
72
- function languageKey(locale) {
73
- return String(locale || 'en').toLowerCase().split(/[-_]/)[0];
74
- }
75
-
76
- function preferredEntityLabel(entry, locale) {
77
- if (!entry) return null;
78
- const key = languageKey(locale);
79
- const aliases = entry.aliases?.[key] || entry.aliases?.all || entry.aliases?.en || [];
80
- return aliases[0] || entry.canonical || null;
81
- }
82
-
83
- function canonicalEntityLabel(catalog, value, locale) {
84
- const entry = catalog.find((item) => item.canonical === value);
85
- return preferredEntityLabel(entry, locale);
86
- }
87
-
88
- export function geographyDisplayName(value, locale = 'en', kind = 'any') {
89
- const text = String(value || '').trim();
90
- if (!text) return '';
91
- const language = languageKey(locale);
92
- const tables = GEOGRAPHY_DISPLAY_NAMES[language];
93
- if (kind === 'country') {
94
- const code = text.toUpperCase();
95
- const explicit = tables?.country?.[code];
96
- if (explicit) return explicit;
97
- return preferredEntityLabel(COUNTRIES.find((item) => item.code === code || item.canonical === text), locale) || text;
98
- }
99
- if (kind === 'city') return tables?.city?.[text] || canonicalEntityLabel(CITIES, text, locale) || text;
100
- if (kind === 'region') return tables?.region?.[text] || canonicalEntityLabel(REGIONS, text, locale) || text;
101
- if (kind === 'district') return tables?.district?.[text] || text;
102
- if (kind === 'metro') return tables?.metro?.[text] || text;
103
- return tables?.country?.[text.toUpperCase()] || tables?.city?.[text] || tables?.region?.[text] || tables?.district?.[text] || tables?.metro?.[text] || canonicalEntityLabel(CITIES, text, locale) || canonicalEntityLabel(REGIONS, text, locale) || preferredEntityLabel(COUNTRIES.find((item) => item.code === text.toUpperCase() || item.canonical === text), locale) || text;
104
- }
105
-
106
- export function geographyMetroLabelWithAlias(value, locale = 'en') {
107
- const text = String(value || '').trim();
108
- if (!text) return '';
109
- const label = geographyDisplayName(text, locale, 'metro');
110
- const tables = GEOGRAPHY_DISPLAY_NAMES[languageKey(locale)];
111
- const alias = tables?.metroAlias?.[text];
112
- return alias && alias !== label ? `${label} (${alias})` : label;
113
- }
1
+ import { COUNTRIES } from './countries.js';
2
+ import { CITIES, REGIONS } from './geography.js';
3
+
4
+ // Centralized presentation names for canonical geography values.
5
+ // Consumers must not maintain their own country/city/district/metro display dictionaries.
6
+
7
+ export const GEOGRAPHY_DISPLAY_NAMES = Object.freeze({
8
+ ru: Object.freeze({
9
+ country: Object.freeze({ UZ: 'Узбекистан', KZ: 'Казахстан', UA: 'Украина', RO: 'Румыния', KG: 'Кыргызстан' }),
10
+ city: Object.freeze({
11
+ Tashkent: 'Ташкент', Samarkand: 'Самарканд', Bukhara: 'Бухара', Namangan: 'Наманган',
12
+ Andijan: 'Андижан', Fergana: 'Фергана', Nukus: 'Нукус', Navoi: 'Навои', Navoiy: 'Навои', Jizzakh: 'Джизак',
13
+ Termez: 'Термез', Qarshi: 'Карши', Urgench: 'Ургенч', Gulistan: 'Гулистан', Chirchiq: 'Чирчик',
14
+ 'Tashkent Region': 'Ташкентская область', Karakalpakstan: 'Каракалпакстан', Kashkadarya: 'Кашкадарья',
15
+ Surkhandarya: 'Сурхандарья', Syrdarya: 'Сырдарья', Khorezm: 'Хорезм',
16
+ Almaty: 'Алматы', Astana: 'Астана', Shymkent: 'Шымкент', Karaganda: 'Караганда',
17
+ Aktobe: 'Актобе', Atyrau: 'Атырау', Oral: 'Уральск', Taraz: 'Тараз', Pavlodar: 'Павлодар',
18
+ Semey: 'Семей', Kostanay: 'Костанай', Kyzylorda: 'Кызылорда', Aktau: 'Актау', Oskemen: 'Усть-Каменогорск',
19
+ Kyiv: 'Киев', Lviv: 'Львов', Odesa: 'Одесса', Kharkiv: 'Харьков', Dnipro: 'Днепр',
20
+ Vinnytsia: 'Винница', 'Ivano-Frankivsk': 'Ивано-Франковск', Lutsk: 'Луцк', Chernivtsi: 'Черновцы',
21
+ Zaporizhzhia: 'Запорожье', Poltava: 'Полтава', Rivne: 'Ровно', Ternopil: 'Тернополь',
22
+ Uzhhorod: 'Ужгород', Khmelnytskyi: 'Хмельницкий', Zhytomyr: 'Житомир', Cherkasy: 'Черкассы',
23
+ Chernihiv: 'Чернигов', Sumy: 'Сумы', Mykolaiv: 'Николаев', Kropyvnytskyi: 'Кропивницкий',
24
+ Kherson: 'Херсон', 'Kryvyi Rih': 'Кривой Рог', Kremenchuk: 'Кременчуг', 'Bila Tserkva': 'Белая Церковь',
25
+ Kamianske: 'Каменское', Vyshneve: 'Вишневое', Boryspil: 'Борисполь', Vyshhorod: 'Вышгород',
26
+ Oleksandriia: 'Александрия', Pavlohrad: 'Павлоград', Nikopol: 'Никополь', Drohobych: 'Дрогобыч',
27
+ Stryi: 'Стрый', Kolomyia: 'Коломыя', Kalush: 'Калуш', 'Kamianets-Podilskyi': 'Каменец-Подольский',
28
+ Izmail: 'Измаил',
29
+ Bucharest: 'Бухарест', 'Cluj-Napoca': 'Клуж-Напока', Timisoara: 'Тимишоара', Iasi: 'Яссы',
30
+ Brasov: 'Брашов', Constanta: 'Констанца', Oradea: 'Орадя', Sibiu: 'Сибиу',
31
+ }),
32
+ district: Object.freeze({
33
+ Chilanzar: 'Чиланзар', Yunusabad: 'Юнусабад', 'Mirzo Ulugbek': 'Мирзо-Улугбек',
34
+ Yakkasaray: 'Яккасарай', Shaykhantahur: 'Шайхантахур', Yashnobod: 'Яшнабад', Sergeli: 'Сергели',
35
+ Uchtepa: 'Учтепа', Mirobod: 'Мирабад', Bektemir: 'Бектемир', Olmazor: 'Алмазар',
36
+ Almaly: 'Алмалинский', Bostandyk: 'Бостандыкский', Medeu: 'Медеуский', Auezov: 'Ауэзовский',
37
+ Turksib: 'Турксибский', Nauryzbay: 'Наурызбайский', Alatau: 'Алатауский', Zhetysu: 'Жетысуский',
38
+ Podil: 'Подол', Pechersk: 'Печерск', Pecherskyi: 'Печерский', Obolon: 'Оболонь',
39
+ Shevchenkivskyi: 'Шевченковский', Solomianskyi: 'Соломенский', Darnytskyi: 'Дарницкий',
40
+ Holosiivskyi: 'Голосеевский', Dniprovskyi: 'Днепровский', Sviatoshynskyi: 'Святошинский',
41
+ Desnianskyi: 'Деснянский', Prymorskyi: 'Приморский', Frankivskyi: 'Франковский',
42
+ Tsentralnyi: 'Центральный', 'Tsentralno-Miskyi': 'Центрально-Городской', Tsentr: 'Центр',
43
+ Prospekt: 'Проспект', Ruska: 'Русская', Hraviton: 'Гравитон', Komarova: 'Комарова', Roscha: 'Роща',
44
+ Sadgora: 'Садгора', Avtovokzal: 'Автовокзал', Pipera: 'Пипера', Militari: 'Милитари',
45
+ 'Drumul Taberei': 'Друмул Таберей', Titan: 'Титан', Berceni: 'Берчень', Floreasca: 'Флоряска',
46
+ Dorobanti: 'Доробанць', Cotroceni: 'Котрочень',
47
+ }),
48
+ metro: Object.freeze({
49
+ 'Buyuk Ipak Yoli': 'Буюк Ипак Йули', Pushkin: 'Пушкин', 'Hamid Olimjon': 'Хамид Алимджан',
50
+ 'Amir Temur Xiyoboni': 'Амир Темур Хиёбони', 'Mustaqillik Maydoni': 'Мустакиллик майдони',
51
+ Paxtakor: 'Пахтакор', 'Xalqlar Dostligi': 'Халклар Дустлиги', 'Milliy Bog': 'Миллий Бог', Novza: 'Новза',
52
+ 'Mirzo Ulugbek': 'Мирзо Улугбек', Chilonzor: 'Чиланзар', Olmazor: 'Алмазар', Choshtepa: 'Чаштепа',
53
+ Ozgarish: 'Узгариш', Sergeli: 'Сергели', Yangihayot: 'Янгихаёт', Chinor: 'Чинар', Beruniy: 'Беруни',
54
+ Tinchlik: 'Тинчлик', Chorsu: 'Чорсу', 'Gafur Gulom': 'Гафур Гулям', 'Alisher Navoi': 'Алишер Навои',
55
+ Ozbekiston: 'Узбекистан', Kosmonavtlar: 'Космонавтлар', Oybek: 'Ойбек', Toshkent: 'Ташкент',
56
+ Mashinasozlar: 'Машинасозлар', Dostlik: 'Дустлик', Turkiston: 'Туркистон', Yunusobod: 'Юнусабад',
57
+ Shahriston: 'Шахристан', Bodomzor: 'Бадамзар', Minor: 'Минор', 'Abdulla Qodiriy': 'Абдулла Кадыри',
58
+ 'Yunus Rajabiy': 'Юнус Раджаби', 'Ming Orik': 'Мингурик', Texnopark: 'Технопарк', Yashnobod: 'Яшнабад',
59
+ Tuzel: 'Тузель', Olmos: 'Алмас', Rohat: 'Рохат', Yangiobod: 'Янгиабад', Qoyliq: 'Куйлюк', Matonat: 'Матонат',
60
+ Qiyot: 'Кият', Tolariq: 'Толарык', Xonobod: 'Хонабад', Quruvchilar: 'Курувчилар', Turon: 'Туран', Qipchoq: 'Кипчак',
61
+ }),
62
+ metroAlias: Object.freeze({
63
+ 'Buyuk Ipak Yoli': 'Максим Горький', Novza: 'Хамза',
64
+ 'Amir Temur Xiyoboni': 'Сквер Октябрьской Революции', 'Mustaqillik Maydoni': 'Площадь Ленина',
65
+ 'Milliy Bog': 'Национальный парк', Yunusobod: 'Фахрийлар чойхонаси',
66
+ 'Xalqlar Dostligi': 'Дружба народов', Ozbekiston: 'Узбекистан', Mashinasozlar: 'Машиностроителей',
67
+ Dostlik: 'Дружба', Tinchlik: 'Мир',
68
+ }),
69
+ }),
70
+ });
71
+
72
+ function languageKey(locale) {
73
+ return String(locale || 'en').toLowerCase().split(/[-_]/)[0];
74
+ }
75
+
76
+ function preferredEntityLabel(entry, locale) {
77
+ if (!entry) return null;
78
+ const key = languageKey(locale);
79
+ const aliases = entry.aliases?.[key] || entry.aliases?.all || entry.aliases?.en || [];
80
+ return aliases[0] || entry.canonical || null;
81
+ }
82
+
83
+ function canonicalEntityLabel(catalog, value, locale) {
84
+ const entry = catalog.find((item) => item.canonical === value);
85
+ return preferredEntityLabel(entry, locale);
86
+ }
87
+
88
+ export function geographyDisplayName(value, locale = 'en', kind = 'any') {
89
+ const text = String(value || '').trim();
90
+ if (!text) return '';
91
+ const language = languageKey(locale);
92
+ const tables = GEOGRAPHY_DISPLAY_NAMES[language];
93
+ if (kind === 'country') {
94
+ const code = text.toUpperCase();
95
+ const explicit = tables?.country?.[code];
96
+ if (explicit) return explicit;
97
+ return preferredEntityLabel(COUNTRIES.find((item) => item.code === code || item.canonical === text), locale) || text;
98
+ }
99
+ if (kind === 'city') return tables?.city?.[text] || canonicalEntityLabel(CITIES, text, locale) || text;
100
+ if (kind === 'region') return tables?.region?.[text] || canonicalEntityLabel(REGIONS, text, locale) || text;
101
+ if (kind === 'district') return tables?.district?.[text] || text;
102
+ if (kind === 'metro') return tables?.metro?.[text] || text;
103
+ return tables?.country?.[text.toUpperCase()] || tables?.city?.[text] || tables?.region?.[text] || tables?.district?.[text] || tables?.metro?.[text] || canonicalEntityLabel(CITIES, text, locale) || canonicalEntityLabel(REGIONS, text, locale) || preferredEntityLabel(COUNTRIES.find((item) => item.code === text.toUpperCase() || item.canonical === text), locale) || text;
104
+ }
105
+
106
+ export function geographyMetroLabelWithAlias(value, locale = 'en') {
107
+ const text = String(value || '').trim();
108
+ if (!text) return '';
109
+ const label = geographyDisplayName(text, locale, 'metro');
110
+ const tables = GEOGRAPHY_DISPLAY_NAMES[languageKey(locale)];
111
+ const alias = tables?.metroAlias?.[text];
112
+ return alias && alias !== label ? `${label} (${alias})` : label;
113
+ }