@whiteslove/parsing-lexicon 0.2.10 → 0.2.14

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 (41) hide show
  1. package/package.json +4 -1
  2. package/src/central-asia-locations.js +10 -28
  3. package/src/central-asia.js +14 -160
  4. package/src/geography-central-asia.js +182 -2
  5. package/src/geography-detection.d.ts +1 -0
  6. package/src/geography-detection.js +42 -2
  7. package/src/geography-display.js +36 -8
  8. package/src/hiring-salary-context.d.ts +4 -0
  9. package/src/hiring-salary-context.js +46 -12
  10. package/src/housing-address.d.ts +10 -1
  11. package/src/housing-address.js +112 -10
  12. package/src/housing-context.js +6 -4
  13. package/src/housing-display.d.ts +2 -0
  14. package/src/housing-display.js +65 -0
  15. package/src/housing-features.js +19 -3
  16. package/src/housing-intent.js +6 -4
  17. package/src/housing-listing-enrichment.d.ts +65 -0
  18. package/src/housing-listing-enrichment.js +253 -0
  19. package/src/housing-listing-fields.js +57 -16
  20. package/src/housing-money.d.ts +2 -1
  21. package/src/housing-money.js +31 -3
  22. package/src/housing-poi-extensions.js +134 -0
  23. package/src/housing-source-aliases.d.ts +12 -0
  24. package/src/housing-source-aliases.js +94 -0
  25. package/src/housing-structured.d.ts +46 -2
  26. package/src/housing-structured.js +200 -22
  27. package/src/housing-text.d.ts +20 -1
  28. package/src/housing-text.js +42 -17
  29. package/src/housing.js +9 -7
  30. package/src/index.js +7 -0
  31. package/src/landmarks.js +10 -3
  32. package/src/location-data.js +410 -0
  33. package/src/location-merge.js +1 -1
  34. package/src/locations.js +48 -426
  35. package/src/tashkent-housing-geography.js +8 -2
  36. package/src/tashkent-pois.js +19 -19
  37. package/src/ua-location-extensions-major.js +30 -18
  38. package/src/ua-location-extensions-metro.js +31 -0
  39. package/src/ua-location-extensions-regional.js +11 -2
  40. package/src/ua-secondary-cities.js +1 -1
  41. package/src/uz-location-extensions.js +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.2.10",
3
+ "version": "0.2.14",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for Whiteslove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,6 +48,9 @@
48
48
  "./housing-listing-fields": { "types": "./src/housing-listing-fields.d.ts", "import": "./src/housing-listing-fields.js" },
49
49
  "./housing-text": { "types": "./src/housing-text.d.ts", "import": "./src/housing-text.js" },
50
50
  "./housing-source-aliases": { "types": "./src/housing-source-aliases.d.ts", "import": "./src/housing-source-aliases.js" },
51
+ "./housing-poi-extensions": "./src/housing-poi-extensions.js",
52
+ "./housing-display": { "types": "./src/housing-display.d.ts", "import": "./src/housing-display.js" },
53
+ "./housing-listing-enrichment": { "types": "./src/housing-listing-enrichment.d.ts", "import": "./src/housing-listing-enrichment.js" },
51
54
  "./hiring": "./src/hiring.js",
52
55
  "./hiring-languages": "./src/hiring-languages.js",
53
56
  "./hiring-language-extensions": { "types": "./src/hiring-language-extensions.d.ts", "import": "./src/hiring-language-extensions.js" },
@@ -1,24 +1,14 @@
1
1
  import { LOCATION_DICTIONARIES } from './locations.js';
2
- import { LOCATION_LIST_KEYS, mergeLocationCountries } from './location-merge.js';
2
+ import { LOCATION_LIST_KEYS } from './location-merge.js';
3
+ import { CITIES_BY_COUNTRY, canonicalCity } from './geography.js';
3
4
  import { aliasesOf, aliasesToRegex, normalizeForMatch } from './normalization.js';
4
- import {
5
- KZ_CITY_CATALOG,
6
- UZ_CITY_CATALOG,
7
- canonicalKazakhstanCity,
8
- canonicalUzbekistanCity,
9
- } from './central-asia.js';
10
- import { KZ_AMBIGUOUS_LOCAL_NAMES, KZ_LOCATION_EXTENSIONS, KZ_SEARCH_CLUSTERS } from './kz-location-extensions.js';
11
- import { UZ_AMBIGUOUS_LOCAL_NAMES, UZ_LOCATION_EXTENSIONS } from './uz-location-extensions.js';
12
-
13
- export const KZ_EXPANDED_LOCATION_DICTIONARIES = mergeLocationCountries(
14
- LOCATION_DICTIONARIES.KZ || {},
15
- KZ_LOCATION_EXTENSIONS,
16
- );
17
-
18
- export const UZ_EXPANDED_LOCATION_DICTIONARIES = mergeLocationCountries(
19
- LOCATION_DICTIONARIES.UZ || {},
20
- UZ_LOCATION_EXTENSIONS,
21
- );
5
+ import { KZ_AMBIGUOUS_LOCAL_NAMES, KZ_SEARCH_CLUSTERS } from './kz-location-extensions.js';
6
+ import { UZ_AMBIGUOUS_LOCAL_NAMES } from './uz-location-extensions.js';
7
+
8
+ // Compatibility names retained for consumers. The canonical dictionaries are
9
+ // assembled once in locations.js; matcher layers must consume, not re-merge.
10
+ export const KZ_EXPANDED_LOCATION_DICTIONARIES = LOCATION_DICTIONARIES.KZ || Object.freeze({});
11
+ export const UZ_EXPANDED_LOCATION_DICTIONARIES = LOCATION_DICTIONARIES.UZ || Object.freeze({});
22
12
 
23
13
  export const CENTRAL_ASIA_LOCATION_DICTIONARIES = Object.freeze({
24
14
  KZ: KZ_EXPANDED_LOCATION_DICTIONARIES,
@@ -49,21 +39,13 @@ const KZ_AMBIGUOUS = new Set(KZ_AMBIGUOUS_LOCAL_NAMES.map(normalizeForMatch));
49
39
  const UZ_AMBIGUOUS = new Set(UZ_AMBIGUOUS_LOCAL_NAMES.map(normalizeForMatch));
50
40
 
51
41
  function cityCatalog(countryCode) {
52
- return countryCode === 'KZ' ? KZ_CITY_CATALOG : countryCode === 'UZ' ? UZ_CITY_CATALOG : [];
42
+ return CITIES_BY_COUNTRY[countryCode] || [];
53
43
  }
54
44
 
55
45
  function dictionaries(countryCode) {
56
46
  return CENTRAL_ASIA_LOCATION_DICTIONARIES[countryCode] || {};
57
47
  }
58
48
 
59
- function canonicalCity(value, countryCode) {
60
- return countryCode === 'KZ'
61
- ? canonicalKazakhstanCity(value)
62
- : countryCode === 'UZ'
63
- ? canonicalUzbekistanCity(value)
64
- : null;
65
- }
66
-
67
49
  const cityRegexCache = new Map();
68
50
  function cityRegex(item) {
69
51
  const key = `${item.country}:${item.canonical}`;
@@ -1,5 +1,11 @@
1
- import { KZ_CITIES, UZ_CITIES } from './geo.js';
2
- import { aliasesOf, findCanonical, normalizeForMatch } from './normalization.js';
1
+ import {
2
+ KZ_CITY_ADDITIONS,
3
+ UZ_CITY_ADDITIONS,
4
+ } from './geography-central-asia.js';
5
+ import { CITIES_BY_COUNTRY, canonicalCity } from './geography.js';
6
+ import { aliasesOf, normalizeForMatch } from './normalization.js';
7
+
8
+ export { KZ_CITY_ADDITIONS, UZ_CITY_ADDITIONS };
3
9
 
4
10
  const freezeAliases = (aliases = {}) => Object.freeze(Object.fromEntries(
5
11
  Object.entries(aliases).map(([lang, values]) => {
@@ -20,117 +26,8 @@ const city = (canonical, aliases, extra = {}) => Object.freeze({
20
26
  aliases: freezeAliases(aliases),
21
27
  });
22
28
 
23
- function mergeCatalog(base, additions) {
24
- const order = [];
25
- const byCanonical = new Map();
26
- for (const item of [...base, ...additions]) {
27
- if (!item?.canonical) continue;
28
- if (!byCanonical.has(item.canonical)) order.push(item.canonical);
29
- const previous = byCanonical.get(item.canonical);
30
- if (!previous) {
31
- byCanonical.set(item.canonical, item);
32
- continue;
33
- }
34
- const languages = [...new Set([...Object.keys(previous.aliases || {}), ...Object.keys(item.aliases || {})])];
35
- const aliases = Object.fromEntries(languages.map((lang) => {
36
- const seen = new Set();
37
- const values = [...(previous.aliases?.[lang] || []), ...(item.aliases?.[lang] || [])].filter((alias) => {
38
- const key = normalizeForMatch(alias);
39
- if (!key || seen.has(key)) return false;
40
- seen.add(key);
41
- return true;
42
- });
43
- return [lang, values];
44
- }));
45
- const { aliases: _previousAliases, ...previousMeta } = previous;
46
- const { aliases: _itemAliases, ...itemMeta } = item;
47
- byCanonical.set(item.canonical, city(item.canonical, aliases, { ...previousMeta, ...itemMeta }));
48
- }
49
- return Object.freeze(order.map((name) => byCanonical.get(name)));
50
- }
51
-
52
- export const KZ_CITY_ADDITIONS = Object.freeze([
53
- city('Almaty', { kk: ['Алматы'], ru: ['Алма-Ата', 'Алма Ата', 'Верный'], en: ['Alma-Ata', 'Alma Ata', 'Verny'] }, { country: 'KZ', priority: 'P0' }),
54
- city('Astana', { kk: ['Астана', 'Нұр-Сұлтан', 'Нұр Сұлтан', 'Ақмола'], ru: ['Астана', 'Нур-Султан', 'Нур Султан', 'Акмолинск', 'Акмола', 'Целиноград'], en: ['Astana', 'Nur-Sultan', 'Nur Sultan', 'Akmolinsk', 'Aqmola', 'Tselinograd'] }, { country: 'KZ', priority: 'P0' }),
55
- city('Shymkent', { kk: ['Шымкент'], ru: ['Шымкент', 'Чимкент', 'Чымкент'], en: ['Shymkent', 'Chimkent'] }, { country: 'KZ', priority: 'P0' }),
56
- city('Karaganda', { kk: ['Қарағанды'], ru: ['Караганда', 'Караганды'], en: ['Karaganda', 'Qaragandy', 'Karagandy'] }, { country: 'KZ', priority: 'P1' }),
57
- city('Aktobe', { kk: ['Ақтөбе'], ru: ['Актобе', 'Актюбинск'], en: ['Aktobe', 'Aqtobe', 'Aktyubinsk'] }, { country: 'KZ', priority: 'P1' }),
58
- city('Taraz', { kk: ['Тараз', 'Жамбыл', 'Әулие-Ата'], ru: ['Тараз', 'Джамбул', 'Жамбыл', 'Аулие-Ата'], en: ['Taraz', 'Dzhambul', 'Jambyl', 'Aulie-Ata'] }, { country: 'KZ', priority: 'P1' }),
59
- city('Pavlodar', { kk: ['Павлодар'], ru: ['Павлодар'], en: ['Pavlodar'] }, { country: 'KZ', priority: 'P1' }),
60
- city('Oskemen', { kk: ['Өскемен'], ru: ['Усть-Каменогорск', 'Усть Каменогорск', 'Оскемен'], en: ['Oskemen', 'Ust-Kamenogorsk', 'Ust Kamenogorsk'] }, { country: 'KZ', priority: 'P1' }),
61
- city('Semey', { kk: ['Семей'], ru: ['Семей', 'Семипалатинск'], en: ['Semey', 'Semipalatinsk'] }, { country: 'KZ', priority: 'P1' }),
62
- city('Atyrau', { kk: ['Атырау'], ru: ['Атырау', 'Гурьев'], en: ['Atyrau', 'Guryev', 'Guriev'] }, { country: 'KZ', priority: 'P1' }),
63
- city('Aktau', { kk: ['Ақтау'], ru: ['Актау', 'Шевченко'], en: ['Aktau', 'Aqtau', 'Shevchenko'] }, { country: 'KZ', priority: 'P1' }),
64
- city('Kostanay', { kk: ['Қостанай'], ru: ['Костанай', 'Кустанай'], en: ['Kostanay', 'Qostanay', 'Kustanay'] }, { country: 'KZ', priority: 'P2' }),
65
- city('Kyzylorda', { kk: ['Қызылорда', 'Ақмешіт'], ru: ['Кызылорда', 'Ак-Мечеть', 'Перовск'], en: ['Kyzylorda', 'Qyzylorda', 'Kzyl-Orda', 'Ak-Mechet', 'Perovsk'] }, { country: 'KZ', priority: 'P2' }),
66
- city('Oral', { kk: ['Орал'], ru: ['Орал', 'Уральск'], en: ['Oral', 'Uralsk'] }, { country: 'KZ', priority: 'P2' }),
67
- city('Petropavl', { kk: ['Петропавл'], ru: ['Петропавловск', 'Петропавл'], en: ['Petropavl', 'Petropavlovsk'] }, { country: 'KZ', priority: 'P2' }),
68
- city('Taldykorgan', { kk: ['Талдықорған'], ru: ['Талдыкорган', 'Талды-Курган'], en: ['Taldykorgan', 'Taldyqorgan', 'Taldy-Kurgan'] }, { country: 'KZ', priority: 'P2' }),
69
- city('Turkistan', { kk: ['Түркістан'], ru: ['Туркестан'], en: ['Turkistan', 'Turkestan'] }, { country: 'KZ', priority: 'P2' }),
70
- city('Kokshetau', { kk: ['Көкшетау'], ru: ['Кокшетау', 'Кокчетав'], en: ['Kokshetau', 'Kokchetav'] }, { country: 'KZ', priority: 'P2' }),
71
- city('Temirtau', { kk: ['Теміртау'], ru: ['Темиртау'], en: ['Temirtau'] }, { country: 'KZ', priority: 'P3' }),
72
- city('Ekibastuz', { kk: ['Екібастұз'], ru: ['Экибастуз'], en: ['Ekibastuz'] }, { country: 'KZ', priority: 'P3' }),
73
- city('Rudny', { kk: ['Рудный'], ru: ['Рудный'], en: ['Rudny', 'Rudnyi'] }, { country: 'KZ', priority: 'P3' }),
74
- city('Zhezkazgan', { kk: ['Жезқазған'], ru: ['Жезказган'], en: ['Zhezkazgan', 'Jezkazgan', 'Dzhezkazgan'] }, { country: 'KZ', priority: 'P3' }),
75
- city('Balkhash', { kk: ['Балқаш'], ru: ['Балхаш'], en: ['Balkhash', 'Balqash'] }, { country: 'KZ', priority: 'P3' }),
76
- city('Konaev', { kk: ['Қонаев', 'Қапшағай'], ru: ['Конаев', 'Капчагай'], en: ['Konaev', 'Qonaev', 'Kapshagay', 'Kapchagai', 'Qapshagai'] }, { country: 'KZ', priority: 'P3' }),
77
- city('Zhanaozen', { kk: ['Жаңаөзен'], ru: ['Жанаозен'], en: ['Zhanaozen', 'Zhanaözen', 'New Uzen'] }, { country: 'KZ', priority: 'P3' }),
78
- city('Satbayev', { kk: ['Сәтбаев'], ru: ['Сатпаев', 'Никольский'], en: ['Satbayev', 'Satpaev', 'Nikolsky'] }, { country: 'KZ', priority: 'P3' }),
79
- city('Kosshy', { kk: ['Қосшы'], ru: ['Косшы'], en: ['Kosshy', 'Qosshy', 'Koschi'] }, { country: 'KZ', priority: 'P3' }),
80
- city('Arys', { kk: ['Арыс'], ru: ['Арыс'], en: ['Arys'] }, { country: 'KZ', priority: 'P3' }),
81
- city('Kentau', { kk: ['Кентау'], ru: ['Кентау'], en: ['Kentau'] }, { country: 'KZ', priority: 'P3' }),
82
- city('Saryagash', { kk: ['Сарыағаш'], ru: ['Сарыагаш'], en: ['Saryagash'] }, { country: 'KZ', priority: 'P3' }),
83
- city('Stepnogorsk', { kk: ['Степногорск'], ru: ['Степногорск'], en: ['Stepnogorsk'] }, { country: 'KZ', priority: 'P4' }),
84
- city('Atbasar', { kk: ['Атбасар'], ru: ['Атбасар'], en: ['Atbasar'] }, { country: 'KZ', priority: 'P4' }),
85
- city('Shchuchinsk', { kk: ['Щучинск'], ru: ['Щучинск'], en: ['Shchuchinsk'] }, { country: 'KZ', priority: 'P4' }),
86
- city('Makinsk', { kk: ['Макинск'], ru: ['Макинск'], en: ['Makinsk'] }, { country: 'KZ', priority: 'P4' }),
87
- city('Ereymentau', { kk: ['Ерейментау'], ru: ['Ерейментау'], en: ['Ereymentau'] }, { country: 'KZ', priority: 'P4' }),
88
- city('Akkol', { kk: ['Ақкөл'], ru: ['Акколь'], en: ['Akkol', 'Aqkol'] }, { country: 'KZ', priority: 'P4' }),
89
- city('Kaskelen', { kk: ['Қаскелең'], ru: ['Каскелен'], en: ['Kaskelen'] }, { country: 'KZ', priority: 'P4' }),
90
- city('Talgar', { kk: ['Талғар'], ru: ['Талгар'], en: ['Talgar'] }, { country: 'KZ', priority: 'P4' }),
91
- city('Esik', { kk: ['Есік'], ru: ['Есик'], en: ['Esik', 'Issyk'] }, { country: 'KZ', priority: 'P4' }),
92
- city('Shelek', { kk: ['Шелек'], ru: ['Шелек'], en: ['Shelek'] }, { country: 'KZ', priority: 'P4' }),
93
- city('Uzynagash', { kk: ['Ұзынағаш'], ru: ['Узынагаш'], en: ['Uzynagash'] }, { country: 'KZ', priority: 'P4' }),
94
- city('Zharkent', { kk: ['Жаркент'], ru: ['Жаркент'], en: ['Zharkent'] }, { country: 'KZ', priority: 'P4' }),
95
- city('Tekeli', { kk: ['Текелі'], ru: ['Текели'], en: ['Tekeli'] }, { country: 'KZ', priority: 'P4' }),
96
- city('Usharal', { kk: ['Үшарал'], ru: ['Ушарал'], en: ['Usharal'] }, { country: 'KZ', priority: 'P4' }),
97
- city('Saran', { kk: ['Саран'], ru: ['Сарань'], en: ['Saran'] }, { country: 'KZ', priority: 'P4' }),
98
- city('Shakhtinsk', { kk: ['Шахтинск'], ru: ['Шахтинск'], en: ['Shakhtinsk'] }, { country: 'KZ', priority: 'P4' }),
99
- city('Abai', { kk: ['Абай'], ru: ['Абай'], en: ['Abai'] }, { country: 'KZ', priority: 'P4' }),
100
- city('Priozersk', { kk: ['Приозерск'], ru: ['Приозёрск'], en: ['Priozersk'] }, { country: 'KZ', priority: 'P4' }),
101
- city('Karkaralinsk', { kk: ['Қарқаралы'], ru: ['Каркаралинск'], en: ['Karkaralinsk', 'Karkaraly'] }, { country: 'KZ', priority: 'P4' }),
102
- city('Aksu', { kk: ['Ақсу'], ru: ['Аксу'], en: ['Aksu', 'Aqsu'] }, { country: 'KZ', priority: 'P4' }),
103
- city('Ridder', { kk: ['Риддер'], ru: ['Риддер'], en: ['Ridder'] }, { country: 'KZ', priority: 'P4' }),
104
- city('Altai', { kk: ['Алтай'], ru: ['Алтай', 'Зыряновск'], en: ['Altai', 'Zyryanovsk'] }, { country: 'KZ', priority: 'P4' }),
105
- city('Serebryansk', { kk: ['Серебрянск'], ru: ['Серебрянск'], en: ['Serebryansk'] }, { country: 'KZ', priority: 'P4' }),
106
- city('Shemonaikha', { kk: ['Шемонаиха'], ru: ['Шемонаиха'], en: ['Shemonaikha'] }, { country: 'KZ', priority: 'P4' }),
107
- city('Kurchatov', { kk: ['Курчатов'], ru: ['Курчатов'], en: ['Kurchatov'] }, { country: 'KZ', priority: 'P4' }),
108
- city('Ayagoz', { kk: ['Аягөз'], ru: ['Аягоз'], en: ['Ayagoz'] }, { country: 'KZ', priority: 'P4' }),
109
- city('Khromtau', { kk: ['Хромтау'], ru: ['Хромтау'], en: ['Khromtau'] }, { country: 'KZ', priority: 'P4' }),
110
- city('Alga', { kk: ['Алға'], ru: ['Алга'], en: ['Alga'] }, { country: 'KZ', priority: 'P4' }),
111
- city('Kandyagash', { kk: ['Қандыағаш'], ru: ['Кандыагаш'], en: ['Kandyagash'] }, { country: 'KZ', priority: 'P4' }),
112
- city('Shalkar', { kk: ['Шалқар'], ru: ['Шалкар'], en: ['Shalkar'] }, { country: 'KZ', priority: 'P4' }),
113
- city('Kulsary', { kk: ['Құлсары'], ru: ['Кульсары'], en: ['Kulsary'] }, { country: 'KZ', priority: 'P4' }),
114
- city('Dossor', { kk: ['Доссор'], ru: ['Доссор'], en: ['Dossor'] }, { country: 'KZ', priority: 'P4' }),
115
- city('Fort-Shevchenko', { kk: ['Форт-Шевченко'], ru: ['Форт-Шевченко'], en: ['Fort-Shevchenko'] }, { country: 'KZ', priority: 'P4' }),
116
- city('Lisakovsk', { kk: ['Лисаковск'], ru: ['Лисаковск'], en: ['Lisakovsk'] }, { country: 'KZ', priority: 'P4' }),
117
- city('Arkalyk', { kk: ['Арқалық'], ru: ['Аркалык'], en: ['Arkalyk'] }, { country: 'KZ', priority: 'P4' }),
118
- city('Tobyl', { kk: ['Тобыл'], ru: ['Тобыл'], en: ['Tobyl'] }, { country: 'KZ', priority: 'P4' }),
119
- city('Zhitikara', { kk: ['Жітіқара'], ru: ['Житикара'], en: ['Zhitikara'] }, { country: 'KZ', priority: 'P4' }),
120
- city('Aksai', { kk: ['Ақсай'], ru: ['Аксай'], en: ['Aksai'] }, { country: 'KZ', priority: 'P4' }),
121
- city('Baikonur', { kk: ['Байқоңыр'], ru: ['Байконур'], en: ['Baikonur'] }, { country: 'KZ', priority: 'P4', type: 'special_status_city' }),
122
- city('Aral', { kk: ['Арал'], ru: ['Аральск', 'Арал'], en: ['Aral', 'Aralsk'] }, { country: 'KZ', priority: 'P4' }),
123
- city('Kazaly', { kk: ['Қазалы'], ru: ['Казалинск', 'Казалы'], en: ['Kazaly', 'Kazalinsk'] }, { country: 'KZ', priority: 'P4' }),
124
- city('Shu', { kk: ['Шу'], ru: ['Шу'], en: ['Shu'] }, { country: 'KZ', priority: 'P4' }),
125
- city('Karatau', { kk: ['Қаратау'], ru: ['Каратау'], en: ['Karatau'] }, { country: 'KZ', priority: 'P4' }),
126
- city('Zhanatas', { kk: ['Жаңатас'], ru: ['Жанатас'], en: ['Zhanatas'] }, { country: 'KZ', priority: 'P4' }),
127
- city('Merke', { kk: ['Мерке'], ru: ['Мерке'], en: ['Merke'] }, { country: 'KZ', priority: 'P4' }),
128
- city('Zhetysai', { kk: ['Жетісай'], ru: ['Жетысай'], en: ['Zhetysai'] }, { country: 'KZ', priority: 'P4' }),
129
- city('Lenger', { kk: ['Леңгір'], ru: ['Ленгер'], en: ['Lenger'] }, { country: 'KZ', priority: 'P4' }),
130
- city('Shardara', { kk: ['Шардара'], ru: ['Шардара'], en: ['Shardara'] }, { country: 'KZ', priority: 'P4' }),
131
- ]);
132
-
133
- export const KZ_CITY_CATALOG = mergeCatalog(KZ_CITIES, KZ_CITY_ADDITIONS);
29
+ /** @deprecated Use CITIES_BY_COUNTRY.KZ / canonicalCity(). */
30
+ export const KZ_CITY_CATALOG = CITIES_BY_COUNTRY.KZ;
134
31
 
135
32
  export const KZ_SEARCH_TARGETS = Object.freeze([
136
33
  city('Burabay', { kk: ['Бурабай'], ru: ['Бурабай', 'Боровое'], en: ['Burabay', 'Borovoe'] }, { country: 'KZ', type: 'resort', priority: 'P4' }),
@@ -138,51 +35,8 @@ export const KZ_SEARCH_TARGETS = Object.freeze([
138
35
  city('Bayanaul', { kk: ['Баянауыл'], ru: ['Баянаул'], en: ['Bayanaul'] }, { country: 'KZ', type: 'resort', priority: 'P4' }),
139
36
  ]);
140
37
 
141
- export const UZ_CITY_ADDITIONS = Object.freeze([
142
- city('Tashkent', { uzLatn: ['Toshkent', 'Toshkent shahri'], uzCyrl: ['Тошкент', 'Тошкент шаҳри'], ru: ['Ташкент', 'город Ташкент'], en: ['Tashkent'] }, { country: 'UZ', priority: 'P0' }),
143
- city('Samarkand', { uzLatn: ['Samarqand', 'Samarqand shahri'], uzCyrl: ['Самарқанд'], ru: ['Самарканд', 'г. Самарканд'], en: ['Samarkand', 'Samarqand'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Samarqand' }),
144
- city('Namangan', { uzLatn: ['Namangan', 'Namangan shahri'], uzCyrl: ['Наманган шаҳри'], ru: ['Наманган'], en: ['Namangan'] }, { country: 'UZ', priority: 'P0' }),
145
- city('Andijan', { uzLatn: ['Andijon', 'Andijon shahri', 'Andijan', 'Andizhan'], uzCyrl: ['Андижон'], ru: ['Андижан'], en: ['Andijan', 'Andizhan'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Andijon' }),
146
- city('Fergana', { uzLatn: ["Farg'ona", 'Fargona', "Farg'ona shahri"], uzCyrl: ['Фарғона', 'Фаргона'], ru: ['Фергана'], en: ['Fergana', 'Ferghana'] }, { country: 'UZ', priority: 'P0', localCanonical: "Farg'ona" }),
147
- city('Bukhara', { uzLatn: ['Buxoro', 'Buxoro shahri'], uzCyrl: ['Бухоро'], ru: ['Бухара'], en: ['Bukhara', 'Buchara'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Buxoro' }),
148
- city('Qarshi', { uzLatn: ['Qarshi', 'Qarshi shahri', 'Karshi'], uzCyrl: ['Қарши'], ru: ['Карши'], en: ['Qarshi', 'Karshi'] }, { country: 'UZ', priority: 'P0' }),
149
- city('Nukus', { uzLatn: ['Nukus', 'Nukus shahri'], uzCyrl: ['Нукус'], kaaLat: ['Nókis'], kaaCyrl: ['Нөкис', 'Нөкис қаласы'], ru: ['Нукус'], en: ['Nukus'] }, { country: 'UZ', priority: 'P0', region: "Qoraqalpog'iston" }),
150
- city('Urgench', { uzLatn: ['Urganch', 'Urganch shahri', 'Urgench'], uzCyrl: ['Урганч'], ru: ['Ургенч'], en: ['Urgench', 'Urganch'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Urganch' }),
151
- city('Kokand', { uzLatn: ["Qo'qon", 'Qo‘qon', 'Qoqon'], uzCyrl: ['Қўқон'], ru: ['Коканд'], en: ['Kokand', 'Kokon'] }, { country: 'UZ', priority: 'P1', localCanonical: "Qo'qon" }),
152
- city('Margilan', { uzLatn: ["Marg'ilon", 'Marg‘ilon', 'Margilon'], uzCyrl: ['Марғилон'], ru: ['Маргилан'], en: ['Margilan', 'Marghelan'] }, { country: 'UZ', priority: 'P1', localCanonical: "Marg'ilon" }),
153
- city('Jizzakh', { uzLatn: ['Jizzax', 'Jizzakh'], uzCyrl: ['Жиззах'], ru: ['Джизак'], en: ['Jizzakh', 'Djizak'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Jizzax' }),
154
- city('Navoiy', { uzLatn: ['Navoiy', 'Navoi'], uzCyrl: ['Навоий'], ru: ['Навои'], en: ['Navoiy', 'Navoi'] }, { country: 'UZ', priority: 'P1' }),
155
- city('Termez', { uzLatn: ['Termiz', 'Termiz shahri'], uzCyrl: ['Термиз'], ru: ['Термез'], en: ['Termez', 'Termiz'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Termiz' }),
156
- city('Gulistan', { uzLatn: ['Guliston', 'Gulistan'], uzCyrl: ['Гулистон'], ru: ['Гулистан'], en: ['Gulistan', 'Guliston'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Guliston' }),
157
- city('Chirchiq', { uzLatn: ['Chirchiq', 'Chirchik'], uzCyrl: ['Чирчиқ'], ru: ['Чирчик'], en: ['Chirchiq', 'Chirchik'] }, { country: 'UZ', priority: 'P1' }),
158
- city('Almalyk', { uzLatn: ['Olmaliq', 'Almalyk'], uzCyrl: ['Олмалиқ'], ru: ['Алмалык'], en: ['Almalyk', 'Olmaliq'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Olmaliq' }),
159
- city('Angren', { uzLatn: ['Angren'], uzCyrl: ['Ангрен'], ru: ['Ангрен'], en: ['Angren'] }, { country: 'UZ', priority: 'P1' }),
160
- city('Bekabad', { uzLatn: ['Bekobod', 'Bekabad'], uzCyrl: ['Бекобод'], ru: ['Бекабад', 'Бекобод'], en: ['Bekabad', 'Bekobod'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Bekobod' }),
161
- city('Shakhrisabz', { uzLatn: ['Shahrisabz', 'Shakhrisabz', 'Shahr-i Sabz'], uzCyrl: ['Шаҳрисабз'], ru: ['Шахрисабз'], en: ['Shakhrisabz', 'Shahrisabz'] }, { country: 'UZ', priority: 'P2', localCanonical: 'Shahrisabz' }),
162
- city('Khiva', { uzLatn: ['Xiva', 'Khiva'], uzCyrl: ['Хива'], ru: ['Хива'], en: ['Khiva', 'Chiwa'] }, { country: 'UZ', priority: 'P2', localCanonical: 'Xiva' }),
163
- city('Denov', { uzLatn: ['Denov', 'Denau'], uzCyrl: ['Денов'], ru: ['Денау', 'Денов'], en: ['Denov', 'Denau'] }, { country: 'UZ', priority: 'P2' }),
164
- city('Asaka', { uzLatn: ['Asaka', 'Assaka'], uzCyrl: ['Асака'], ru: ['Асака'], en: ['Asaka'] }, { country: 'UZ', priority: 'P2' }),
165
- city('Kogon', { uzLatn: ['Kogon', 'Yangi Buxoro'], uzCyrl: ['Когон'], ru: ['Каган', 'Когон', 'Новая Бухара'], en: ['Kogon', 'Kagan'] }, { country: 'UZ', priority: 'P2' }),
166
- city('Kattakurgan', { uzLatn: ["Kattaqo'rg'on", 'Kattaqo‘rg‘on', 'Kattakurgan'], uzCyrl: ['Каттақўрғон'], ru: ['Каттакурган'], en: ['Kattakurgan'] }, { country: 'UZ', priority: 'P2', localCanonical: "Kattaqo'rg'on" }),
167
- city('Urgut', { uzLatn: ['Urgut', 'Urgut shahri'], uzCyrl: ['Ургут'], ru: ['Ургут'], en: ['Urgut'] }, { country: 'UZ', priority: 'P2' }),
168
- city('Yangiyol', { uzLatn: ["Yangiyo'l", 'Yangiyo‘l', 'Yangiyul'], uzCyrl: ['Янги йўл', 'Янгийўл'], ru: ['Янгиюль'], en: ['Yangiyol', 'Yangiyul'] }, { country: 'UZ', priority: 'P2', localCanonical: "Yangiyo'l" }),
169
- city('Chust', { uzLatn: ['Chust'], uzCyrl: ['Чуст'], ru: ['Чуст'], en: ['Chust'] }, { country: 'UZ', priority: 'P3' }),
170
- city('Chartak', { uzLatn: ['Chortoq', 'Chartak'], uzCyrl: ['Чортоқ'], ru: ['Чартак'], en: ['Chartak', 'Chortoq'] }, { country: 'UZ', priority: 'P3', localCanonical: 'Chortoq' }),
171
- city('Kosonsoy', { uzLatn: ['Kosonsoy', 'Kasansay'], uzCyrl: ['Косонсой'], ru: ['Касансай', 'Косонсой'], en: ['Kosonsoy', 'Kasansay'] }, { country: 'UZ', priority: 'P3' }),
172
- city('Shahrixon', { uzLatn: ['Shahrixon', 'Shahrikhan'], uzCyrl: ['Шаҳрихон'], ru: ['Шахрихан'], en: ['Shahrixon', 'Shahrikhan'] }, { country: 'UZ', priority: 'P3' }),
173
- city('Xonobod', { uzLatn: ['Xonobod', 'Xonobod shahri'], uzCyrl: ['Хонобод'], ru: ['Ханабад', 'Хонобод'], en: ['Khanabad', 'Xonobod'] }, { country: 'UZ', priority: 'P3', contextRequired: true }),
174
- city('Khojeyli', { uzLatn: ["Xo'jayli", 'Xojayli'], uzCyrl: ['Хўжайли'], kaaLat: ['Xojeli'], ru: ['Ходжейли'], en: ['Khojeyli', 'Khodzheyli'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
175
- city('Takhiatash', { uzLatn: ['Taxiatosh'], uzCyrl: ['Тахиатош'], kaaLat: ['Taxiatas'], ru: ['Тахиаташ'], en: ['Takhiatash', 'Tahiatash'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
176
- city('Kungrad', { uzLatn: ["Qo'ng'irot", 'Qo‘ng‘irot'], uzCyrl: ['Қўнғирот'], kaaLat: ['Qońırat'], ru: ['Кунград'], en: ['Kungrad', 'Kongirat'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
177
- city('Beruniy', { uzLatn: ['Beruniy'], uzCyrl: ['Беруний'], ru: ['Беруни'], en: ['Beruniy', 'Biruni'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
178
- city('Turtkul', { uzLatn: ["To'rtko'l", 'To‘rtko‘l'], uzCyrl: ['Тўрткўл'], kaaLat: ['Tórtkúl'], ru: ['Турткуль'], en: ['Turtkul', 'Tortkul'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
179
- city('Yangiyer', { uzLatn: ['Yangiyer', 'Yangi Yer'], uzCyrl: ['Янгиер'], ru: ['Янгиер'], en: ['Yangiyer'] }, { country: 'UZ', priority: 'P3' }),
180
- city('Shirin', { uzLatn: ['Shirin'], uzCyrl: ['Ширин'], ru: ['Ширин'], en: ['Shirin'] }, { country: 'UZ', priority: 'P3' }),
181
- city('Gazalkent', { uzLatn: ["G'azalkent", 'Gazalkent'], uzCyrl: ['Ғазалкент'], ru: ['Газалкент'], en: ['Gazalkent'] }, { country: 'UZ', priority: 'P3', localCanonical: "G'azalkent" }),
182
- city('Muynak', { uzLatn: ["Mo'ynoq", 'Moynaq'], uzCyrl: ['Мўйноқ'], kaaLat: ['Moynaq'], ru: ['Муйнак'], en: ['Muynak', 'Moynak'] }, { country: 'UZ', priority: 'P4', region: "Qoraqalpog'iston" }),
183
- ]);
184
-
185
- export const UZ_CITY_CATALOG = mergeCatalog(UZ_CITIES, UZ_CITY_ADDITIONS);
38
+ /** @deprecated Use CITIES_BY_COUNTRY.UZ / canonicalCity(). */
39
+ export const UZ_CITY_CATALOG = CITIES_BY_COUNTRY.UZ;
186
40
 
187
41
  export const UZ_SEARCH_TARGETS = Object.freeze([
188
42
  city('Chorvoq', { uzLatn: ['Chorvoq', 'Charvak'], ru: ['Чарвак'], en: ['Chorvoq', 'Charvak'] }, { country: 'UZ', type: 'tourism_target', priority: 'P4' }),
@@ -213,11 +67,11 @@ export const UZ_LOCATION_TERMS = Object.freeze({
213
67
  });
214
68
 
215
69
  export function canonicalKazakhstanCity(value) {
216
- return findCanonical(value, KZ_CITY_CATALOG)?.canonical || null;
70
+ return canonicalCity(value, 'KZ');
217
71
  }
218
72
 
219
73
  export function canonicalUzbekistanCity(value) {
220
- return findCanonical(value, UZ_CITY_CATALOG)?.canonical || null;
74
+ return canonicalCity(value, 'UZ');
221
75
  }
222
76
 
223
77
  export function canonicalCentralAsiaCity(value, country = null) {
@@ -1,8 +1,57 @@
1
1
  import { lexiconEntity } from './lexicon-core.js';
2
+ import { normalizeForMatch } from './normalization.js';
2
3
 
3
4
  const entity = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, { type: 'city', ...extra });
4
5
 
5
- export const UZ_CITIES = Object.freeze([
6
+ const freezeAliases = (aliases = {}) => Object.freeze(Object.fromEntries(
7
+ Object.entries(aliases).map(([lang, values]) => {
8
+ const seen = new Set();
9
+ const deduped = (values || []).filter((alias) => {
10
+ const key = normalizeForMatch(alias);
11
+ if (!key || seen.has(key)) return false;
12
+ seen.add(key);
13
+ return true;
14
+ });
15
+ return [lang, Object.freeze(deduped)];
16
+ }),
17
+ ));
18
+
19
+ const catalogCity = (canonical, aliases, extra = {}) => Object.freeze({
20
+ ...extra,
21
+ canonical,
22
+ aliases: freezeAliases(aliases),
23
+ });
24
+
25
+ function mergeCatalog(base, additions) {
26
+ const order = [];
27
+ const byCanonical = new Map();
28
+ for (const item of [...base, ...additions]) {
29
+ if (!item?.canonical) continue;
30
+ if (!byCanonical.has(item.canonical)) order.push(item.canonical);
31
+ const previous = byCanonical.get(item.canonical);
32
+ if (!previous) {
33
+ byCanonical.set(item.canonical, item);
34
+ continue;
35
+ }
36
+ const languages = [...new Set([...Object.keys(previous.aliases || {}), ...Object.keys(item.aliases || {})])];
37
+ const aliases = Object.fromEntries(languages.map((lang) => {
38
+ const seen = new Set();
39
+ const values = [...(previous.aliases?.[lang] || []), ...(item.aliases?.[lang] || [])].filter((alias) => {
40
+ const key = normalizeForMatch(alias);
41
+ if (!key || seen.has(key)) return false;
42
+ seen.add(key);
43
+ return true;
44
+ });
45
+ return [lang, values];
46
+ }));
47
+ const { aliases: _previousAliases, ...previousMeta } = previous;
48
+ const { aliases: _itemAliases, ...itemMeta } = item;
49
+ byCanonical.set(item.canonical, catalogCity(item.canonical, aliases, { ...previousMeta, ...itemMeta }));
50
+ }
51
+ return Object.freeze(order.map((name) => byCanonical.get(name)));
52
+ }
53
+
54
+ const UZ_BASE_CITIES = Object.freeze([
6
55
  entity('Tashkent', { uzLatn: ['Toshkent'], uzCyrl: ['Тошкент'], ru: ['Ташкент'], en: ['Tashkent'] }, { country: 'UZ' }),
7
56
  entity('Samarkand', { uzLatn: ['Samarqand'], uzCyrl: ['Самарқанд'], ru: ['Самарканд'], en: ['Samarkand'] }, { country: 'UZ' }),
8
57
  entity('Bukhara', { uzLatn: ['Buxoro'], uzCyrl: ['Бухоро'], ru: ['Бухара'], en: ['Bukhara'] }, { country: 'UZ' }),
@@ -20,7 +69,7 @@ export const UZ_CITIES = Object.freeze([
20
69
  entity('Chirchiq', { uzLatn: ['Chirchiq', 'Chirchik'], uzCyrl: ['Чирчиқ'], ru: ['Чирчик'], en: ['Chirchiq'] }, { country: 'UZ' }),
21
70
  ]);
22
71
 
23
- export const KZ_CITIES = Object.freeze([
72
+ const KZ_BASE_CITIES = Object.freeze([
24
73
  entity('Almaty', { kk: ['Алматы'], ru: ['Алматы', 'Алма-Ата', 'Алма Ата'], en: ['Almaty', 'Alma-Ata', 'Alma Ata'] }, { country: 'KZ' }),
25
74
  entity('Astana', { kk: ['Астана', 'Нұр-Сұлтан', 'Нұр Сұлтан'], ru: ['Астана', 'Нур-Султан', 'Нур Султан'], en: ['Astana', 'Nur-Sultan', 'Nur Sultan'] }, { country: 'KZ' }),
26
75
  entity('Shymkent', { kk: ['Шымкент'], ru: ['Шымкент', 'Чимкент'], en: ['Shymkent', 'Chimkent'] }, { country: 'KZ' }),
@@ -40,3 +89,134 @@ export const KZ_CITIES = Object.freeze([
40
89
  entity('Taldykorgan', { kk: ['Талдықорған'], ru: ['Талдыкорган'], en: ['Taldykorgan', 'Taldyqorgan'] }, { country: 'KZ' }),
41
90
  entity('Kokshetau', { kk: ['Көкшетау'], ru: ['Кокшетау'], en: ['Kokshetau'] }, { country: 'KZ' }),
42
91
  ]);
92
+
93
+ export const KZ_CITY_ADDITIONS = Object.freeze([
94
+ catalogCity('Almaty', { kk: ['Алматы'], ru: ['Алма-Ата', 'Алма Ата', 'Верный'], en: ['Alma-Ata', 'Alma Ata', 'Verny'] }, { country: 'KZ', priority: 'P0' }),
95
+ catalogCity('Astana', { kk: ['Астана', 'Нұр-Сұлтан', 'Нұр Сұлтан', 'Ақмола'], ru: ['Астана', 'Нур-Султан', 'Нур Султан', 'Акмолинск', 'Акмола', 'Целиноград'], en: ['Astana', 'Nur-Sultan', 'Nur Sultan', 'Akmolinsk', 'Aqmola', 'Tselinograd'] }, { country: 'KZ', priority: 'P0' }),
96
+ catalogCity('Shymkent', { kk: ['Шымкент'], ru: ['Шымкент', 'Чимкент', 'Чымкент'], en: ['Shymkent', 'Chimkent'] }, { country: 'KZ', priority: 'P0' }),
97
+ catalogCity('Karaganda', { kk: ['Қарағанды'], ru: ['Караганда', 'Караганды'], en: ['Karaganda', 'Qaragandy', 'Karagandy'] }, { country: 'KZ', priority: 'P1' }),
98
+ catalogCity('Aktobe', { kk: ['Ақтөбе'], ru: ['Актобе', 'Актюбинск'], en: ['Aktobe', 'Aqtobe', 'Aktyubinsk'] }, { country: 'KZ', priority: 'P1' }),
99
+ catalogCity('Taraz', { kk: ['Тараз', 'Жамбыл', 'Әулие-Ата'], ru: ['Тараз', 'Джамбул', 'Жамбыл', 'Аулие-Ата'], en: ['Taraz', 'Dzhambul', 'Jambyl', 'Aulie-Ata'] }, { country: 'KZ', priority: 'P1' }),
100
+ catalogCity('Pavlodar', { kk: ['Павлодар'], ru: ['Павлодар'], en: ['Pavlodar'] }, { country: 'KZ', priority: 'P1' }),
101
+ catalogCity('Oskemen', { kk: ['Өскемен'], ru: ['Усть-Каменогорск', 'Усть Каменогорск', 'Оскемен'], en: ['Oskemen', 'Ust-Kamenogorsk', 'Ust Kamenogorsk'] }, { country: 'KZ', priority: 'P1' }),
102
+ catalogCity('Semey', { kk: ['Семей'], ru: ['Семей', 'Семипалатинск'], en: ['Semey', 'Semipalatinsk'] }, { country: 'KZ', priority: 'P1' }),
103
+ catalogCity('Atyrau', { kk: ['Атырау'], ru: ['Атырау', 'Гурьев'], en: ['Atyrau', 'Guryev', 'Guriev'] }, { country: 'KZ', priority: 'P1' }),
104
+ catalogCity('Aktau', { kk: ['Ақтау'], ru: ['Актау', 'Шевченко'], en: ['Aktau', 'Aqtau', 'Shevchenko'] }, { country: 'KZ', priority: 'P1' }),
105
+ catalogCity('Kostanay', { kk: ['Қостанай'], ru: ['Костанай', 'Кустанай'], en: ['Kostanay', 'Qostanay', 'Kustanay'] }, { country: 'KZ', priority: 'P2' }),
106
+ catalogCity('Kyzylorda', { kk: ['Қызылорда', 'Ақмешіт'], ru: ['Кызылорда', 'Ак-Мечеть', 'Перовск'], en: ['Kyzylorda', 'Qyzylorda', 'Kzyl-Orda', 'Ak-Mechet', 'Perovsk'] }, { country: 'KZ', priority: 'P2' }),
107
+ catalogCity('Oral', { kk: ['Орал'], ru: ['Орал', 'Уральск'], en: ['Oral', 'Uralsk'] }, { country: 'KZ', priority: 'P2' }),
108
+ catalogCity('Petropavl', { kk: ['Петропавл'], ru: ['Петропавловск', 'Петропавл'], en: ['Petropavl', 'Petropavlovsk'] }, { country: 'KZ', priority: 'P2' }),
109
+ catalogCity('Taldykorgan', { kk: ['Талдықорған'], ru: ['Талдыкорган', 'Талды-Курган'], en: ['Taldykorgan', 'Taldyqorgan', 'Taldy-Kurgan'] }, { country: 'KZ', priority: 'P2' }),
110
+ catalogCity('Turkistan', { kk: ['Түркістан'], ru: ['Туркестан'], en: ['Turkistan', 'Turkestan'] }, { country: 'KZ', priority: 'P2' }),
111
+ catalogCity('Kokshetau', { kk: ['Көкшетау'], ru: ['Кокшетау', 'Кокчетав'], en: ['Kokshetau', 'Kokchetav'] }, { country: 'KZ', priority: 'P2' }),
112
+ catalogCity('Temirtau', { kk: ['Теміртау'], ru: ['Темиртау'], en: ['Temirtau'] }, { country: 'KZ', priority: 'P3' }),
113
+ catalogCity('Ekibastuz', { kk: ['Екібастұз'], ru: ['Экибастуз'], en: ['Ekibastuz'] }, { country: 'KZ', priority: 'P3' }),
114
+ catalogCity('Rudny', { kk: ['Рудный'], ru: ['Рудный'], en: ['Rudny', 'Rudnyi'] }, { country: 'KZ', priority: 'P3' }),
115
+ catalogCity('Zhezkazgan', { kk: ['Жезқазған'], ru: ['Жезказган'], en: ['Zhezkazgan', 'Jezkazgan', 'Dzhezkazgan'] }, { country: 'KZ', priority: 'P3' }),
116
+ catalogCity('Balkhash', { kk: ['Балқаш'], ru: ['Балхаш'], en: ['Balkhash', 'Balqash'] }, { country: 'KZ', priority: 'P3' }),
117
+ catalogCity('Konaev', { kk: ['Қонаев', 'Қапшағай'], ru: ['Конаев', 'Капчагай'], en: ['Konaev', 'Qonaev', 'Kapshagay', 'Kapchagai', 'Qapshagai'] }, { country: 'KZ', priority: 'P3' }),
118
+ catalogCity('Zhanaozen', { kk: ['Жаңаөзен'], ru: ['Жанаозен'], en: ['Zhanaozen', 'Zhanaözen', 'New Uzen'] }, { country: 'KZ', priority: 'P3' }),
119
+ catalogCity('Satbayev', { kk: ['Сәтбаев'], ru: ['Сатпаев', 'Никольский'], en: ['Satbayev', 'Satpaev', 'Nikolsky'] }, { country: 'KZ', priority: 'P3' }),
120
+ catalogCity('Kosshy', { kk: ['Қосшы'], ru: ['Косшы'], en: ['Kosshy', 'Qosshy', 'Koschi'] }, { country: 'KZ', priority: 'P3' }),
121
+ catalogCity('Arys', { kk: ['Арыс'], ru: ['Арыс'], en: ['Arys'] }, { country: 'KZ', priority: 'P3' }),
122
+ catalogCity('Kentau', { kk: ['Кентау'], ru: ['Кентау'], en: ['Kentau'] }, { country: 'KZ', priority: 'P3' }),
123
+ catalogCity('Saryagash', { kk: ['Сарыағаш'], ru: ['Сарыагаш'], en: ['Saryagash'] }, { country: 'KZ', priority: 'P3' }),
124
+ catalogCity('Stepnogorsk', { kk: ['Степногорск'], ru: ['Степногорск'], en: ['Stepnogorsk'] }, { country: 'KZ', priority: 'P4' }),
125
+ catalogCity('Atbasar', { kk: ['Атбасар'], ru: ['Атбасар'], en: ['Atbasar'] }, { country: 'KZ', priority: 'P4' }),
126
+ catalogCity('Shchuchinsk', { kk: ['Щучинск'], ru: ['Щучинск'], en: ['Shchuchinsk'] }, { country: 'KZ', priority: 'P4' }),
127
+ catalogCity('Makinsk', { kk: ['Макинск'], ru: ['Макинск'], en: ['Makinsk'] }, { country: 'KZ', priority: 'P4' }),
128
+ catalogCity('Ereymentau', { kk: ['Ерейментау'], ru: ['Ерейментау'], en: ['Ereymentau'] }, { country: 'KZ', priority: 'P4' }),
129
+ catalogCity('Akkol', { kk: ['Ақкөл'], ru: ['Акколь'], en: ['Akkol', 'Aqkol'] }, { country: 'KZ', priority: 'P4' }),
130
+ catalogCity('Kaskelen', { kk: ['Қаскелең'], ru: ['Каскелен'], en: ['Kaskelen'] }, { country: 'KZ', priority: 'P4' }),
131
+ catalogCity('Talgar', { kk: ['Талғар'], ru: ['Талгар'], en: ['Talgar'] }, { country: 'KZ', priority: 'P4' }),
132
+ catalogCity('Esik', { kk: ['Есік'], ru: ['Есик'], en: ['Esik', 'Issyk'] }, { country: 'KZ', priority: 'P4' }),
133
+ catalogCity('Shelek', { kk: ['Шелек'], ru: ['Шелек'], en: ['Shelek'] }, { country: 'KZ', priority: 'P4' }),
134
+ catalogCity('Uzynagash', { kk: ['Ұзынағаш'], ru: ['Узынагаш'], en: ['Uzynagash'] }, { country: 'KZ', priority: 'P4' }),
135
+ catalogCity('Zharkent', { kk: ['Жаркент'], ru: ['Жаркент'], en: ['Zharkent'] }, { country: 'KZ', priority: 'P4' }),
136
+ catalogCity('Tekeli', { kk: ['Текелі'], ru: ['Текели'], en: ['Tekeli'] }, { country: 'KZ', priority: 'P4' }),
137
+ catalogCity('Usharal', { kk: ['Үшарал'], ru: ['Ушарал'], en: ['Usharal'] }, { country: 'KZ', priority: 'P4' }),
138
+ catalogCity('Saran', { kk: ['Саран'], ru: ['Сарань'], en: ['Saran'] }, { country: 'KZ', priority: 'P4' }),
139
+ catalogCity('Shakhtinsk', { kk: ['Шахтинск'], ru: ['Шахтинск'], en: ['Shakhtinsk'] }, { country: 'KZ', priority: 'P4' }),
140
+ catalogCity('Abai', { kk: ['Абай'], ru: ['Абай'], en: ['Abai'] }, { country: 'KZ', priority: 'P4' }),
141
+ catalogCity('Priozersk', { kk: ['Приозерск'], ru: ['Приозёрск'], en: ['Priozersk'] }, { country: 'KZ', priority: 'P4' }),
142
+ catalogCity('Karkaralinsk', { kk: ['Қарқаралы'], ru: ['Каркаралинск'], en: ['Karkaralinsk', 'Karkaraly'] }, { country: 'KZ', priority: 'P4' }),
143
+ catalogCity('Aksu', { kk: ['Ақсу'], ru: ['Аксу'], en: ['Aksu', 'Aqsu'] }, { country: 'KZ', priority: 'P4' }),
144
+ catalogCity('Ridder', { kk: ['Риддер'], ru: ['Риддер'], en: ['Ridder'] }, { country: 'KZ', priority: 'P4' }),
145
+ catalogCity('Altai', { kk: ['Алтай'], ru: ['Алтай', 'Зыряновск'], en: ['Altai', 'Zyryanovsk'] }, { country: 'KZ', priority: 'P4' }),
146
+ catalogCity('Serebryansk', { kk: ['Серебрянск'], ru: ['Серебрянск'], en: ['Serebryansk'] }, { country: 'KZ', priority: 'P4' }),
147
+ catalogCity('Shemonaikha', { kk: ['Шемонаиха'], ru: ['Шемонаиха'], en: ['Shemonaikha'] }, { country: 'KZ', priority: 'P4' }),
148
+ catalogCity('Kurchatov', { kk: ['Курчатов'], ru: ['Курчатов'], en: ['Kurchatov'] }, { country: 'KZ', priority: 'P4' }),
149
+ catalogCity('Ayagoz', { kk: ['Аягөз'], ru: ['Аягоз'], en: ['Ayagoz'] }, { country: 'KZ', priority: 'P4' }),
150
+ catalogCity('Khromtau', { kk: ['Хромтау'], ru: ['Хромтау'], en: ['Khromtau'] }, { country: 'KZ', priority: 'P4' }),
151
+ catalogCity('Alga', { kk: ['Алға'], ru: ['Алга'], en: ['Alga'] }, { country: 'KZ', priority: 'P4' }),
152
+ catalogCity('Kandyagash', { kk: ['Қандыағаш'], ru: ['Кандыагаш'], en: ['Kandyagash'] }, { country: 'KZ', priority: 'P4' }),
153
+ catalogCity('Shalkar', { kk: ['Шалқар'], ru: ['Шалкар'], en: ['Shalkar'] }, { country: 'KZ', priority: 'P4' }),
154
+ catalogCity('Kulsary', { kk: ['Құлсары'], ru: ['Кульсары'], en: ['Kulsary'] }, { country: 'KZ', priority: 'P4' }),
155
+ catalogCity('Dossor', { kk: ['Доссор'], ru: ['Доссор'], en: ['Dossor'] }, { country: 'KZ', priority: 'P4' }),
156
+ catalogCity('Fort-Shevchenko', { kk: ['Форт-Шевченко'], ru: ['Форт-Шевченко'], en: ['Fort-Shevchenko'] }, { country: 'KZ', priority: 'P4' }),
157
+ catalogCity('Lisakovsk', { kk: ['Лисаковск'], ru: ['Лисаковск'], en: ['Lisakovsk'] }, { country: 'KZ', priority: 'P4' }),
158
+ catalogCity('Arkalyk', { kk: ['Арқалық'], ru: ['Аркалык'], en: ['Arkalyk'] }, { country: 'KZ', priority: 'P4' }),
159
+ catalogCity('Tobyl', { kk: ['Тобыл'], ru: ['Тобыл'], en: ['Tobyl'] }, { country: 'KZ', priority: 'P4' }),
160
+ catalogCity('Zhitikara', { kk: ['Жітіқара'], ru: ['Житикара'], en: ['Zhitikara'] }, { country: 'KZ', priority: 'P4' }),
161
+ catalogCity('Aksai', { kk: ['Ақсай'], ru: ['Аксай'], en: ['Aksai'] }, { country: 'KZ', priority: 'P4' }),
162
+ catalogCity('Baikonur', { kk: ['Байқоңыр'], ru: ['Байконур'], en: ['Baikonur'] }, { country: 'KZ', priority: 'P4', type: 'special_status_city' }),
163
+ catalogCity('Aral', { kk: ['Арал'], ru: ['Аральск', 'Арал'], en: ['Aral', 'Aralsk'] }, { country: 'KZ', priority: 'P4' }),
164
+ catalogCity('Kazaly', { kk: ['Қазалы'], ru: ['Казалинск', 'Казалы'], en: ['Kazaly', 'Kazalinsk'] }, { country: 'KZ', priority: 'P4' }),
165
+ catalogCity('Shu', { kk: ['Шу'], ru: ['Шу'], en: ['Shu'] }, { country: 'KZ', priority: 'P4' }),
166
+ catalogCity('Karatau', { kk: ['Қаратау'], ru: ['Каратау'], en: ['Karatau'] }, { country: 'KZ', priority: 'P4' }),
167
+ catalogCity('Zhanatas', { kk: ['Жаңатас'], ru: ['Жанатас'], en: ['Zhanatas'] }, { country: 'KZ', priority: 'P4' }),
168
+ catalogCity('Merke', { kk: ['Мерке'], ru: ['Мерке'], en: ['Merke'] }, { country: 'KZ', priority: 'P4' }),
169
+ catalogCity('Zhetysai', { kk: ['Жетісай'], ru: ['Жетысай'], en: ['Zhetysai'] }, { country: 'KZ', priority: 'P4' }),
170
+ catalogCity('Lenger', { kk: ['Леңгір'], ru: ['Ленгер'], en: ['Lenger'] }, { country: 'KZ', priority: 'P4' }),
171
+ catalogCity('Shardara', { kk: ['Шардара'], ru: ['Шардара'], en: ['Shardara'] }, { country: 'KZ', priority: 'P4' }),
172
+ ]);
173
+
174
+ export const UZ_CITY_ADDITIONS = Object.freeze([
175
+ catalogCity('Tashkent', { uzLatn: ['Toshkent', 'Toshkent shahri'], uzCyrl: ['Тошкент', 'Тошкент шаҳри'], ru: ['Ташкент', 'город Ташкент'], en: ['Tashkent'] }, { country: 'UZ', priority: 'P0' }),
176
+ catalogCity('Samarkand', { uzLatn: ['Samarqand', 'Samarqand shahri'], uzCyrl: ['Самарқанд'], ru: ['Самарканд', 'г. Самарканд'], en: ['Samarkand', 'Samarqand'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Samarqand' }),
177
+ catalogCity('Namangan', { uzLatn: ['Namangan', 'Namangan shahri'], uzCyrl: ['Наманган шаҳри'], ru: ['Наманган'], en: ['Namangan'] }, { country: 'UZ', priority: 'P0' }),
178
+ catalogCity('Andijan', { uzLatn: ['Andijon', 'Andijon shahri', 'Andijan', 'Andizhan'], uzCyrl: ['Андижон'], ru: ['Андижан'], en: ['Andijan', 'Andizhan'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Andijon' }),
179
+ catalogCity('Fergana', { uzLatn: ["Farg'ona", 'Fargona', "Farg'ona shahri"], uzCyrl: ['Фарғона', 'Фаргона'], ru: ['Фергана'], en: ['Fergana', 'Ferghana'] }, { country: 'UZ', priority: 'P0', localCanonical: "Farg'ona" }),
180
+ catalogCity('Bukhara', { uzLatn: ['Buxoro', 'Buxoro shahri'], uzCyrl: ['Бухоро'], ru: ['Бухара'], en: ['Bukhara', 'Buchara'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Buxoro' }),
181
+ catalogCity('Qarshi', { uzLatn: ['Qarshi', 'Qarshi shahri', 'Karshi'], uzCyrl: ['Қарши'], ru: ['Карши'], en: ['Qarshi', 'Karshi'] }, { country: 'UZ', priority: 'P0' }),
182
+ catalogCity('Nukus', { uzLatn: ['Nukus', 'Nukus shahri'], uzCyrl: ['Нукус'], kaaLat: ['Nókis'], kaaCyrl: ['Нөкис', 'Нөкис қаласы'], ru: ['Нукус'], en: ['Nukus'] }, { country: 'UZ', priority: 'P0', region: "Qoraqalpog'iston" }),
183
+ catalogCity('Urgench', { uzLatn: ['Urganch', 'Urganch shahri', 'Urgench'], uzCyrl: ['Урганч'], ru: ['Ургенч'], en: ['Urgench', 'Urganch'] }, { country: 'UZ', priority: 'P0', localCanonical: 'Urganch' }),
184
+ catalogCity('Kokand', { uzLatn: ["Qo'qon", 'Qo‘qon', 'Qoqon'], uzCyrl: ['Қўқон'], ru: ['Коканд'], en: ['Kokand', 'Kokon'] }, { country: 'UZ', priority: 'P1', localCanonical: "Qo'qon" }),
185
+ catalogCity('Margilan', { uzLatn: ["Marg'ilon", 'Marg‘ilon', 'Margilon'], uzCyrl: ['Марғилон'], ru: ['Маргилан'], en: ['Margilan', 'Marghelan'] }, { country: 'UZ', priority: 'P1', localCanonical: "Marg'ilon" }),
186
+ catalogCity('Jizzakh', { uzLatn: ['Jizzax', 'Jizzakh'], uzCyrl: ['Жиззах'], ru: ['Джизак'], en: ['Jizzakh', 'Djizak'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Jizzax' }),
187
+ catalogCity('Navoiy', { uzLatn: ['Navoiy', 'Navoi'], uzCyrl: ['Навоий'], ru: ['Навои'], en: ['Navoiy', 'Navoi'] }, { country: 'UZ', priority: 'P1' }),
188
+ catalogCity('Termez', { uzLatn: ['Termiz', 'Termiz shahri'], uzCyrl: ['Термиз'], ru: ['Термез'], en: ['Termez', 'Termiz'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Termiz' }),
189
+ catalogCity('Gulistan', { uzLatn: ['Guliston', 'Gulistan'], uzCyrl: ['Гулистон'], ru: ['Гулистан'], en: ['Gulistan', 'Guliston'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Guliston' }),
190
+ catalogCity('Chirchiq', { uzLatn: ['Chirchiq', 'Chirchik'], uzCyrl: ['Чирчиқ'], ru: ['Чирчик'], en: ['Chirchiq', 'Chirchik'] }, { country: 'UZ', priority: 'P1' }),
191
+ catalogCity('Almalyk', { uzLatn: ['Olmaliq', 'Almalyk'], uzCyrl: ['Олмалиқ'], ru: ['Алмалык'], en: ['Almalyk', 'Olmaliq'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Olmaliq' }),
192
+ catalogCity('Angren', { uzLatn: ['Angren'], uzCyrl: ['Ангрен'], ru: ['Ангрен'], en: ['Angren'] }, { country: 'UZ', priority: 'P1' }),
193
+ catalogCity('Bekabad', { uzLatn: ['Bekobod', 'Bekabad'], uzCyrl: ['Бекобод'], ru: ['Бекабад', 'Бекобод'], en: ['Bekabad', 'Bekobod'] }, { country: 'UZ', priority: 'P1', localCanonical: 'Bekobod' }),
194
+ catalogCity('Shakhrisabz', { uzLatn: ['Shahrisabz', 'Shakhrisabz', 'Shahr-i Sabz'], uzCyrl: ['Шаҳрисабз'], ru: ['Шахрисабз'], en: ['Shakhrisabz', 'Shahrisabz'] }, { country: 'UZ', priority: 'P2', localCanonical: 'Shahrisabz' }),
195
+ catalogCity('Khiva', { uzLatn: ['Xiva', 'Khiva'], uzCyrl: ['Хива'], ru: ['Хива'], en: ['Khiva', 'Chiwa'] }, { country: 'UZ', priority: 'P2', localCanonical: 'Xiva' }),
196
+ catalogCity('Denov', { uzLatn: ['Denov', 'Denau'], uzCyrl: ['Денов'], ru: ['Денау', 'Денов'], en: ['Denov', 'Denau'] }, { country: 'UZ', priority: 'P2' }),
197
+ catalogCity('Asaka', { uzLatn: ['Asaka', 'Assaka'], uzCyrl: ['Асака'], ru: ['Асака'], en: ['Asaka'] }, { country: 'UZ', priority: 'P2' }),
198
+ catalogCity('Kogon', { uzLatn: ['Kogon', 'Yangi Buxoro'], uzCyrl: ['Когон'], ru: ['Каган', 'Когон', 'Новая Бухара'], en: ['Kogon', 'Kagan'] }, { country: 'UZ', priority: 'P2' }),
199
+ catalogCity('Kattakurgan', { uzLatn: ["Kattaqo'rg'on", 'Kattaqo‘rg‘on', 'Kattakurgan'], uzCyrl: ['Каттақўрғон'], ru: ['Каттакурган'], en: ['Kattakurgan'] }, { country: 'UZ', priority: 'P2', localCanonical: "Kattaqo'rg'on" }),
200
+ catalogCity('Urgut', { uzLatn: ['Urgut', 'Urgut shahri'], uzCyrl: ['Ургут'], ru: ['Ургут'], en: ['Urgut'] }, { country: 'UZ', priority: 'P2' }),
201
+ catalogCity('Yangiyol', { uzLatn: ["Yangiyo'l", 'Yangiyo‘l', 'Yangiyul'], uzCyrl: ['Янги йўл', 'Янгийўл'], ru: ['Янгиюль'], en: ['Yangiyol', 'Yangiyul'] }, { country: 'UZ', priority: 'P2', localCanonical: "Yangiyo'l" }),
202
+ catalogCity('Chust', { uzLatn: ['Chust'], uzCyrl: ['Чуст'], ru: ['Чуст'], en: ['Chust'] }, { country: 'UZ', priority: 'P3' }),
203
+ catalogCity('Chartak', { uzLatn: ['Chortoq', 'Chartak'], uzCyrl: ['Чортоқ'], ru: ['Чартак'], en: ['Chartak', 'Chortoq'] }, { country: 'UZ', priority: 'P3', localCanonical: 'Chortoq' }),
204
+ catalogCity('Kosonsoy', { uzLatn: ['Kosonsoy', 'Kasansay'], uzCyrl: ['Косонсой'], ru: ['Касансай', 'Косонсой'], en: ['Kosonsoy', 'Kasansay'] }, { country: 'UZ', priority: 'P3' }),
205
+ catalogCity('Shahrixon', { uzLatn: ['Shahrixon', 'Shahrikhan'], uzCyrl: ['Шаҳрихон'], ru: ['Шахрихан'], en: ['Shahrixon', 'Shahrikhan'] }, { country: 'UZ', priority: 'P3' }),
206
+ catalogCity('Xonobod', { uzLatn: ['Xonobod', 'Xonobod shahri'], uzCyrl: ['Хонобод'], ru: ['Ханабад', 'Хонобод'], en: ['Khanabad', 'Xonobod'] }, { country: 'UZ', priority: 'P3', contextRequired: true }),
207
+ catalogCity('Khojeyli', { uzLatn: ["Xo'jayli", 'Xojayli'], uzCyrl: ['Хўжайли'], kaaLat: ['Xojeli'], ru: ['Ходжейли'], en: ['Khojeyli', 'Khodzheyli'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
208
+ catalogCity('Takhiatash', { uzLatn: ['Taxiatosh'], uzCyrl: ['Тахиатош'], kaaLat: ['Taxiatas'], ru: ['Тахиаташ'], en: ['Takhiatash', 'Tahiatash'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
209
+ catalogCity('Kungrad', { uzLatn: ["Qo'ng'irot", 'Qo‘ng‘irot'], uzCyrl: ['Қўнғирот'], kaaLat: ['Qońırat'], ru: ['Кунград'], en: ['Kungrad', 'Kongirat'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
210
+ catalogCity('Beruniy', { uzLatn: ['Beruniy'], uzCyrl: ['Беруний'], ru: ['Беруни'], en: ['Beruniy', 'Biruni'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
211
+ catalogCity('Turtkul', { uzLatn: ["To'rtko'l", 'To‘rtko‘l'], uzCyrl: ['Тўрткўл'], kaaLat: ['Tórtkúl'], ru: ['Турткуль'], en: ['Turtkul', 'Tortkul'] }, { country: 'UZ', priority: 'P3', region: "Qoraqalpog'iston" }),
212
+ catalogCity('Yangiyer', { uzLatn: ['Yangiyer', 'Yangi Yer'], uzCyrl: ['Янгиер'], ru: ['Янгиер'], en: ['Yangiyer'] }, { country: 'UZ', priority: 'P3' }),
213
+ catalogCity('Shirin', { uzLatn: ['Shirin'], uzCyrl: ['Ширин'], ru: ['Ширин'], en: ['Shirin'] }, { country: 'UZ', priority: 'P3' }),
214
+ catalogCity('Gazalkent', { uzLatn: ["G'azalkent", 'Gazalkent'], uzCyrl: ['Ғазалкент'], ru: ['Газалкент'], en: ['Gazalkent'] }, { country: 'UZ', priority: 'P3', localCanonical: "G'azalkent" }),
215
+ catalogCity('Muynak', { uzLatn: ["Mo'ynoq", 'Moynaq'], uzCyrl: ['Мўйноқ'], kaaLat: ['Moynaq'], ru: ['Муйнак'], en: ['Muynak', 'Moynak'] }, { country: 'UZ', priority: 'P4', region: "Qoraqalpog'iston" }),
216
+ ]);
217
+
218
+ /** Canonical Kazakhstan city catalog used by every geography consumer. */
219
+ export const KZ_CITIES = mergeCatalog(KZ_BASE_CITIES, KZ_CITY_ADDITIONS);
220
+
221
+ /** Canonical Uzbekistan city catalog used by every geography consumer. */
222
+ export const UZ_CITIES = mergeCatalog(UZ_BASE_CITIES, UZ_CITY_ADDITIONS);
@@ -12,3 +12,4 @@ export const HIRING_GLOBAL_CITIES: readonly Readonly<{
12
12
 
13
13
  export function detectCountryCodeFromText(value: unknown): string | null;
14
14
  export function detectCityFromText(value: unknown, country?: string | null): DetectedCity | null;
15
+ export function detectCitiesFromText(value: unknown, country?: string | null): readonly DetectedCity[];
@@ -33,6 +33,21 @@ const COUNTRY_MATCHERS = COUNTRIES.map((item) => {
33
33
  });
34
34
 
35
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
+ const CITY_CONTEXT_RE = /(?:shahr(?:i)?|город(?:а|е|у|ом)?|city|viloyat(?:i)?|област\p{L}{0,4})/iu;
37
+
38
+ function cityTextMatch(text, matcher) {
39
+ const exact = text.match(matcher.exact);
40
+ const inflected = exact || !matcher.inflected ? null : text.match(matcher.inflected);
41
+ const match = exact || inflected;
42
+ if (!match) return null;
43
+ if (!matcher.item.contextRequired) return match;
44
+
45
+ const start = match.index ?? 0;
46
+ const end = start + match[0].length;
47
+ const before = text.slice(Math.max(0, start - 40), start);
48
+ const after = text.slice(end, end + 48);
49
+ return CITY_CONTEXT_RE.test(`${before} ${match[0]} ${after}`) ? match : null;
50
+ }
36
51
 
37
52
  /** Detect an ISO-2 country from a country alias, known city alias, or US city/state location. */
38
53
  export function detectCountryCodeFromText(value) {
@@ -57,8 +72,33 @@ export function detectCityFromText(value, country = null) {
57
72
  const text = String(value || '');
58
73
  if (!text) return null;
59
74
  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))));
75
+ const match = CITY_MATCHERS.find((matcher) =>
76
+ (!code || matcher.item.country === code) && Boolean(cityTextMatch(text, matcher)));
62
77
  if (!match) return null;
63
78
  return Object.freeze({ canonical: match.item.canonical, country: match.item.country || null });
64
79
  }
80
+
81
+ /** Detect every known city in free text, ordered by first mention and deduplicated by canonical name. */
82
+ export function detectCitiesFromText(value, country = null) {
83
+ const text = String(value || '');
84
+ if (!text) return Object.freeze([]);
85
+ const code = country ? canonicalCountryCode(country) : null;
86
+ const found = [];
87
+ for (const matcher of CITY_MATCHERS) {
88
+ if (code && matcher.item.country !== code) continue;
89
+ const match = cityTextMatch(text, matcher);
90
+ if (!match) continue;
91
+ found.push({
92
+ canonical: matcher.item.canonical,
93
+ country: matcher.item.country || null,
94
+ start: match.index ?? 0,
95
+ });
96
+ }
97
+ found.sort((a, b) => a.start - b.start || a.canonical.localeCompare(b.canonical));
98
+ const seen = new Set();
99
+ return Object.freeze(found.filter((item) => {
100
+ if (seen.has(item.canonical)) return false;
101
+ seen.add(item.canonical);
102
+ return true;
103
+ }).map(({ canonical, country: cityCountry }) => Object.freeze({ canonical, country: cityCountry })));
104
+ }