@whiteslove/parsing-lexicon 0.6.1 → 0.6.4

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 +5 -1
  2. package/src/central-asia-locations.js +3 -3
  3. package/src/central-asia.js +26 -2
  4. package/src/geo.js +7 -7
  5. package/src/geography-display.js +1 -1
  6. package/src/geography-zone-display.js +1 -1
  7. package/src/geography.js +13 -4
  8. package/src/hiring-requirements.js +5 -5
  9. package/src/hiring-safety-blacklist.js +85 -0
  10. package/src/hiring-safety.js +7 -82
  11. package/src/hiring-salary-context.js +6 -2
  12. package/src/hiring-temporal.js +4 -0
  13. package/src/housing-address.js +1 -1
  14. package/src/housing-commercial.js +9 -0
  15. package/src/housing-display.js +14 -1
  16. package/src/housing-listing-enrichment.js +8 -8
  17. package/src/housing-money.js +1 -1
  18. package/src/housing-source-aliases.js +3 -0
  19. package/src/index.js +2 -0
  20. package/src/kg-location-extensions.js +91 -11
  21. package/src/kz-district-extensions.js +36 -0
  22. package/src/kz-location-extensions.js +3 -3
  23. package/src/locations-runtime.js +5 -0
  24. package/src/locations.js +127 -39
  25. package/src/money-core.js +1 -1
  26. package/src/money-lexicon.js +1 -1
  27. package/src/money.js +16 -1
  28. package/src/normalization.js +39 -4
  29. package/src/tashkent-colloquial.js +8 -1
  30. package/src/tashkent-gastronomic-streets.js +108 -0
  31. package/src/tashkent-pois.js +38 -18
  32. package/src/ua-city-location-expansions.js +88 -0
  33. package/src/ua-geo-coordinates.d.ts +14 -0
  34. package/src/ua-geo-coordinates.js +209 -0
  35. package/src/ua-geo-resolver.d.ts +31 -0
  36. package/src/ua-geo-resolver.js +171 -0
  37. package/src/ua-location-extensions-metro.js +42 -1
  38. package/src/ua-location-extensions-regional.js +2 -2
  39. package/src/ua-secondary-cities.js +6 -4
  40. package/src/uz-geo-coordinates.d.ts +14 -0
  41. package/src/uz-geo-coordinates.js +166 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,9 @@
37
37
  },
38
38
  "./central-asia": "./src/central-asia.js",
39
39
  "./ukraine": "./src/ukraine.js",
40
+ "./ua-geo-coordinates": { "types": "./src/ua-geo-coordinates.d.ts", "import": "./src/ua-geo-coordinates.js" },
41
+ "./ua-geo-resolver": { "types": "./src/ua-geo-resolver.d.ts", "import": "./src/ua-geo-resolver.js" },
42
+ "./uz-geo-coordinates": { "types": "./src/uz-geo-coordinates.d.ts", "import": "./src/uz-geo-coordinates.js" },
40
43
  "./location-merge": "./src/location-merge.js",
41
44
  "./locations": "./src/locations-runtime.js",
42
45
  "./kg-location-extensions": "./src/kg-location-extensions.js",
@@ -50,6 +53,7 @@
50
53
  "./tashkent-colloquial": "./src/tashkent-colloquial.js",
51
54
  "./tashkent-residential-complexes": "./src/tashkent-residential-complexes.js",
52
55
  "./tashkent-pois": "./src/tashkent-pois.js",
56
+ "./tashkent-gastronomic-streets": "./src/tashkent-gastronomic-streets.js",
53
57
  "./tashkent-housing-geography": {
54
58
  "types": "./src/tashkent-housing-geography.d.ts",
55
59
  "import": "./src/tashkent-housing-geography.js"
@@ -1,4 +1,4 @@
1
- import { LOCATION_DICTIONARIES } from './locations.js';
1
+ import { LOCATION_DICTIONARIES } from './locations-runtime.js';
2
2
  import { LOCATION_LIST_KEYS } from './location-merge.js';
3
3
  import { CITIES_BY_COUNTRY, canonicalCity } from './geography.js';
4
4
  import { aliasesOf, aliasesToRegex, normalizeForMatch } from './normalization.js';
@@ -6,7 +6,7 @@ import { KZ_AMBIGUOUS_LOCAL_NAMES, KZ_SEARCH_CLUSTERS } from './kz-location-exte
6
6
  import { UZ_AMBIGUOUS_LOCAL_NAMES } from './uz-location-extensions.js';
7
7
 
8
8
  // Compatibility names retained for consumers. The canonical dictionaries are
9
- // assembled once in locations.js; matcher layers must consume, not re-merge.
9
+ // assembled once in the public runtime registry; matcher layers must consume, not re-merge.
10
10
  export const KZ_EXPANDED_LOCATION_DICTIONARIES = LOCATION_DICTIONARIES.KZ || Object.freeze({});
11
11
  export const UZ_EXPANDED_LOCATION_DICTIONARIES = LOCATION_DICTIONARIES.UZ || Object.freeze({});
12
12
 
@@ -274,4 +274,4 @@ export function matchCentralAsiaLocationEntities(text, countryCode, preferredCit
274
274
  export function matchCentralAsiaLocationEntity(text, countryCode, preferredCity = null, type = null) {
275
275
  const result = matchCentralAsiaLocationEntities(text, countryCode, preferredCity);
276
276
  return result.matches.find((item) => !type || item.type === type || item.key === type) || null;
277
- }
277
+ }
@@ -51,6 +51,9 @@ export const UZ_SEARCH_TARGETS = Object.freeze([
51
51
  city("Ellikqal'a", { uzLatn: ["Ellikqal'a", 'Ellikkala'], kaaLat: ['Elliqala'], ru: ['Элликкала'], en: ['Ellikkala'] }, { country: 'UZ', type: 'search_target', priority: 'P4', region: "Qoraqalpog'iston" }),
52
52
  ]);
53
53
 
54
+ /** Canonical Kyrgyzstan city catalog used by geography consumers. */
55
+ export const KG_CITY_CATALOG = CITIES_BY_COUNTRY.KG;
56
+
54
57
  export const KZ_LOCATION_TERMS = Object.freeze({
55
58
  microdistrict: Object.freeze(['микрорайон', 'микр.', 'микр', 'мкр.', 'мкр', 'м-н', 'мкр-н', 'ықшамаудан', 'ықш.ауд.', 'ықш']),
56
59
  residentialArea: Object.freeze(['жилой массив', 'жилмассив', 'ж/м', 'ж.м.', 'тұрғын алабы', 'тұрғын массиві', 'массив']),
@@ -66,6 +69,14 @@ export const UZ_LOCATION_TERMS = Object.freeze({
66
69
  residentialComplex: Object.freeze(['ЖК', 'жк', 'turar joy majmuasi', 'residential complex', 'residence']),
67
70
  });
68
71
 
72
+ export const KG_LOCATION_TERMS = Object.freeze({
73
+ city: Object.freeze(['шаар', 'шаары', 'город', 'г.', 'city']),
74
+ district: Object.freeze(['район', 'району', 'р-н', 'district']),
75
+ microdistrict: Object.freeze(['микрорайон', 'мкр', 'мкр.', 'кичирайон', 'кичи район']),
76
+ residentialArea: Object.freeze(['жилмассив', 'жилой массив', 'массив', 'конуш', 'жаңы конуш']),
77
+ residentialComplex: Object.freeze(['ЖК', 'жк', 'жилой комплекс', 'турак жай комплекси', 'residential complex', 'residence']),
78
+ });
79
+
69
80
  export function canonicalKazakhstanCity(value) {
70
81
  return canonicalCity(value, 'KZ');
71
82
  }
@@ -74,14 +85,27 @@ export function canonicalUzbekistanCity(value) {
74
85
  return canonicalCity(value, 'UZ');
75
86
  }
76
87
 
88
+ export function canonicalKyrgyzstanCity(value) {
89
+ return canonicalCity(value, 'KG');
90
+ }
91
+
77
92
  export function canonicalCentralAsiaCity(value, country = null) {
78
93
  if (country === 'KZ') return canonicalKazakhstanCity(value);
79
94
  if (country === 'UZ') return canonicalUzbekistanCity(value);
80
- return canonicalKazakhstanCity(value) || canonicalUzbekistanCity(value);
95
+ if (country === 'KG') return canonicalKyrgyzstanCity(value);
96
+ return canonicalKazakhstanCity(value)
97
+ || canonicalUzbekistanCity(value)
98
+ || canonicalKyrgyzstanCity(value);
81
99
  }
82
100
 
83
101
  export function centralAsiaCityAliases(canonical, country) {
84
- const catalog = country === 'KZ' ? KZ_CITY_CATALOG : country === 'UZ' ? UZ_CITY_CATALOG : [...KZ_CITY_CATALOG, ...UZ_CITY_CATALOG];
102
+ const catalog = country === 'KZ'
103
+ ? KZ_CITY_CATALOG
104
+ : country === 'UZ'
105
+ ? UZ_CITY_CATALOG
106
+ : country === 'KG'
107
+ ? KG_CITY_CATALOG
108
+ : [...KZ_CITY_CATALOG, ...UZ_CITY_CATALOG, ...KG_CITY_CATALOG];
85
109
  const item = catalog.find((entry) => entry.canonical === canonical);
86
110
  return item ? Object.freeze([...new Set([item.canonical, ...aliasesOf(item)])]) : Object.freeze([]);
87
111
  }
package/src/geo.js CHANGED
@@ -48,7 +48,7 @@ export const TASHKENT_METRO = Object.freeze([
48
48
  station('Hamid Olimjon', 'Хамид Алимджан', 'Hamid Olimjon', 'chilonzor', ['Хамид Олимжон', 'Hamid Alimjan', 'Hamid Alimjon']),
49
49
  station('Amir Temur Xiyoboni', 'Амир Темур Хиёбони', 'Amir Temur Xiyoboni', 'chilonzor', ['Сквер', 'Amir Timur Square', 'Amir Temur']),
50
50
  station('Mustaqillik Maydoni', 'Мустакиллик майдони', 'Mustaqillik Maydoni', 'chilonzor', ['Мустақиллик майдони', 'Площадь Независимости', 'Independence Square']),
51
- station('Paxtakor', 'Пахтакор', 'Paxtakor', 'chilonzor', ['Pakhtakor']),
51
+ station('Paxtakor', 'Пахтакор', 'Paxtakor', 'chilonzor'),
52
52
  station('Xalqlar Dostligi', 'Халклар Дустлиги', 'Xalqlar Dostligi', 'chilonzor', ["Xalqlar Do'stligi", 'Xalqlar Do‘stligi', 'Xalqlar Doʻstligi', 'Халклар Дўстлиги', 'Халқлар Дўстлиги', 'Дружба народов', 'Bunyodkor', 'Бунёдкор']),
53
53
  station('Milliy Bog', 'Миллий Бог', 'Milliy Bog', 'chilonzor', ["Milliy Bog'", 'Milliy Bog‘', 'Milliy Bogʻ', 'Миллий Боғ', 'Yoshlik', 'Ёшлик']),
54
54
  station('Novza', 'Новза', 'Novza', 'chilonzor', ['Хамза', 'Hamza', 'Hamza metro', 'метро Хамза']),
@@ -123,9 +123,9 @@ export const TASHKENT_AREAS = Object.freeze({
123
123
  area('Karakamysh-2/4', ['каракамыш 2 4', 'қорақамиш 2 4', 'qoraqamish 2 4', 'karakamish 2 4']),
124
124
  area('Karakamysh-2/5', ['каракамыш 2 5', 'қорақамиш 2 5', 'qoraqamish 2 5', 'karakamish 2 5']),
125
125
  area('Olympia', ['олимпия', 'olimpiya', 'olympia']), area('Vuzgorodok', ['вузгородок', 'вуз городок', 'vuzgorodok']),
126
- area('Medgorodok', ['медгородок', 'мед городок', 'medgorodok']),
127
- area('Chimbay', ['чимбай', 'chimboy', 'chimbay']),
128
- area('Takhtapul', ['тахтапуль', 'тахтапул', 'taxtapul', 'takhtapul']),
126
+ area('Medgorodok', ['медгородок', 'мед городок', 'medgorodok', 'TashMI-2', 'TashMI 2', 'ToshMI-2', 'ToshMI 2', 'ТашМИ-2', 'ТашМИ 2', 'ТошМИ-2', 'ТошМИ 2', 'новый ташми', 'новый ташми-2', 'янги ташми', 'yangi tashmi']),
127
+ area('Chimbay', ['чимбай', 'чимбой', 'chimboy', 'chimbay']),
128
+ area('Taxtapul', ['тахтапуль', 'тахтапул', 'taxtapul', 'takhtapul', 'Takhtapul']),
129
129
  ]),
130
130
  Mirobod: Object.freeze([
131
131
  area('Hospitalny', ['госпитальный', 'госпиталка', 'hospitalny']),
@@ -140,13 +140,13 @@ export const TASHKENT_AREAS = Object.freeze({
140
140
  area('Traktorsozlar-2', ['тракторсозлар 2', 'traktorsozlar 2', 'ттз 2', 'ttz 2']),
141
141
  area('TTZ-3', ['ттз 3', 'ttz 3']),
142
142
  area('Traktorsozlar-4', ['тракторсозлар 4', 'traktorsozlar 4', 'ттз 4', 'ttz 4']),
143
- area('Yalangach', ['ялангач', 'yalangach', "yalang'och"]), area('Feruza', ['феруза', 'feruza']), area('Geofizika', ['геофизика', 'поселок геофизиков', 'geofizika']),
143
+ area('Yalangach', ['ялангач', 'ялангач массив', 'yalangach', "yalang'och", "yalang'och dahasi", 'высоковольтный', 'массив высоковольтный']), area('Feruza', ['феруза', 'feruza']), area('Geofizika', ['геофизика', 'поселок геофизиков', 'geofizika']),
144
144
  ]),
145
145
  Uchtepa: Object.freeze([
146
146
  area('Lolazor', ['лолазор', 'lolazor']),
147
147
  ]),
148
148
  Sergeli: Object.freeze([
149
- area('Yangi Sergeli', ['янги сергели', 'yangi sergeli']), area('Stroygorod', ['стройгород', 'stroygorod']),
149
+ area('Yangi Sergeli', ['янги сергели', 'yangi sergeli']),
150
150
  area('Uzgarish', ['узгарыш', 'ўзгариш', "o'zgarish", 'uzgarish']),
151
151
  ]),
152
152
  Chilanzar: Object.freeze([
@@ -176,4 +176,4 @@ export const TASHKENT_AREAS = Object.freeze({
176
176
  ...[1, 2, 3, 4].map((n) => area(`Tuzel-${n}`, [`тузель ${n}`, `tuzel ${n}`])),
177
177
  area('Tashselmash', ['ташсельмаш', 'tashselmash']), area('Alimkent', ['алимкент', 'olimkent', 'alimkent']), area('Shohimardon', ['шохимардон', 'shohimardon']),
178
178
  ]),
179
- });
179
+ });
@@ -1,6 +1,6 @@
1
1
  import { COUNTRIES, canonicalCountryCode, countryByCode } from './countries.js';
2
2
  import { CITIES, REGIONS } from './geography.js';
3
- import { dictionaryFor } from './locations.js';
3
+ import { dictionaryFor } from './locations-runtime.js';
4
4
  import { aliasesOf } from './normalization.js';
5
5
 
6
6
  // Centralized presentation names for canonical geography values.
@@ -1,5 +1,5 @@
1
1
  import { geographyDisplayName } from './geography-display.js';
2
- import { dictionaryFor } from './locations.js';
2
+ import { dictionaryFor } from './locations-runtime.js';
3
3
  import { aliasesOf, findCanonical } from './normalization.js';
4
4
  import { canonicalCountryCode } from './countries.js';
5
5
 
package/src/geography.js CHANGED
@@ -57,10 +57,19 @@ const RO_CITIES_BASE = Object.freeze([
57
57
  export const RO_CITIES = Object.freeze([...RO_CITIES_BASE, ...RO_CITY_EXTENSIONS]);
58
58
 
59
59
  export const KG_CITIES = Object.freeze([
60
- entity('Bishkek', { ru: ['Бишкек'], en: ['Bishkek'], kk: ['Бішкек'], uzLatn: ['Bishkek'] }, { country: 'KG', type: 'city' }),
61
- entity('Osh', { ru: ['Ош'], en: ['Osh'], kk: ['Ош'], uzLatn: ['Osh'] }, { country: 'KG', type: 'city' }),
62
- entity('Karakol', { ru: ['Каракол'], en: ['Karakol'], kk: ['Қаракөл'], uzLatn: ['Karakol'] }, { country: 'KG', type: 'city' }),
63
-
60
+ entity('Bishkek', { ky: ['Бишкек'], ru: ['Бишкек'], en: ['Bishkek'], kk: ['Бішкек'], uzLatn: ['Bishkek'] }, { country: 'KG', type: 'city' }),
61
+ entity('Osh', { ky: ['Ош'], ru: ['Ош'], en: ['Osh'], kk: ['Ош'], uzLatn: ['Osh'] }, { country: 'KG', type: 'city' }),
62
+ entity('Jalal-Abad', { ky: ['Жалал-Абад', 'Жалал Абад'], ru: ['Джалал-Абад', 'Джалал Абад'], en: ['Jalal-Abad', 'Jalalabad'] }, { country: 'KG', type: 'city' }),
63
+ entity('Karakol', { ky: ['Каракол'], ru: ['Каракол', 'Пржевальск'], en: ['Karakol', 'Przhevalsk'], kk: ['Қаракөл'], uzLatn: ['Karakol'] }, { country: 'KG', type: 'city' }),
64
+ entity('Tokmok', { ky: ['Токмок'], ru: ['Токмок'], en: ['Tokmok'] }, { country: 'KG', type: 'city' }),
65
+ entity('Naryn', { ky: ['Нарын'], ru: ['Нарын'], en: ['Naryn'] }, { country: 'KG', type: 'city' }),
66
+ entity('Talas', { ky: ['Талас'], ru: ['Талас'], en: ['Talas'] }, { country: 'KG', type: 'city' }),
67
+ entity('Batken', { ky: ['Баткен'], ru: ['Баткен'], en: ['Batken'] }, { country: 'KG', type: 'city' }),
68
+ entity('Kara-Balta', { ky: ['Кара-Балта', 'Кара Балта'], ru: ['Кара-Балта', 'Кара Балта'], en: ['Kara-Balta', 'Kara Balta'] }, { country: 'KG', type: 'city' }),
69
+ entity('Balykchy', { ky: ['Балыкчы'], ru: ['Балыкчи'], en: ['Balykchy', 'Balykchi'] }, { country: 'KG', type: 'city' }),
70
+ entity('Kant', { ky: ['Кант'], ru: ['Кант'], en: ['Kant'] }, { country: 'KG', type: 'city' }),
71
+ entity('Uzgen', { ky: ['Өзгөн', 'Озгон'], ru: ['Узген', 'Озгон'], en: ['Uzgen', 'Özgön', 'Ozgon'] }, { country: 'KG', type: 'city' }),
72
+ entity('Kyzyl-Kiya', { ky: ['Кызыл-Кыя', 'Кызыл Кыя'], ru: ['Кызыл-Кия', 'Кызыл Кия'], en: ['Kyzyl-Kiya', 'Kyzyl Kiya'] }, { country: 'KG', type: 'city' }),
64
73
  ]);
65
74
 
66
75
  /** Canonical cities used across global hiring and housing ingestion. */
@@ -21,12 +21,12 @@ export function detectHiringSeniority(value) {
21
21
  ['director', /\b(?:engineering\s+)?director\b|директор/i],
22
22
  ['principal', /\bprincipal\b(?=[^,;\n]{0,60}\b(?:engineer|developer|architect)\b)/i],
23
23
  ['staff', /\bstaff\b(?=[^,;\n]{0,60}\b(?:engineer|developer|architect)\b)/i],
24
- ['lead', /\b(?:team\s*lead|tech\s*lead|lead\s+(?:engineer|developer|frontend|backend))\b|тимлид|техлид|ведущ\w*/i],
24
+ ['lead', /\b(?:team\s*lead|tech\s*lead|lead\s+(?:engineer|developer|frontend|backend))\b|тимлид|техлид|ведущ\w*|yetakchi\s+(?:dasturchi|mutaxassis|muhandis)|жетекші\s+(?:бағдарламашы|маман|инженер)/iu],
25
25
  ['manager', /\bengineering manager\b|руководител/i],
26
- ['senior', /\bsenior\b(?=[^,;\n]{0,60}\b(?:engineer|developer|frontend|backend|software)\b)|сеньор|старш\w*\s+(?:разработ|инженер)/i],
27
- ['middle', /\b(?:middle|mid[- ]?level)\b(?=[^,;\n]{0,60}\b(?:engineer|developer|frontend|backend|software)\b)|мидл/i],
28
- ['junior', /\bjunior\b(?=[^,;\n]{0,60}\b(?:engineer|developer|frontend|backend|software)\b)|джун\w*|младш\w*/i],
29
- ['intern', /\b(?:intern|trainee)\b|стаж[ёе]р|стажир/i],
26
+ ['senior', /\bsenior\b(?=[^,;\n]{0,60}\b(?:engineer|developer|frontend|backend|software)\b)|сеньор|старш\w*\s+(?:разработ|инженер)|katta\s+(?:dasturchi|mutaxassis|muhandis)|аға\s+(?:бағдарламашы|маман|инженер)/iu],
27
+ ['middle', /\b(?:middle|mid[- ]?level)\b(?=[^,;\n]{0,60}\b(?:engineer|developer|frontend|backend|software)\b)|мидл|o['’ʻ]?rta\s+(?:dasturchi|mutaxassis|muhandis)|орта\s+(?:бағдарламашы|маман|инженер)/iu],
28
+ ['junior', /\bjunior\b(?=[^,;\n]{0,60}\b(?:engineer|developer|frontend|backend|software)\b)|джун\w*|младш\w*|kichik\s+(?:dasturchi|mutaxassis|muhandis)|кіші\s+(?:бағдарламашы|маман|инженер)/iu],
29
+ ['intern', /\b(?:intern|trainee)\b|стаж[ёе]р|стажир|amaliyotchi|stajyor|тәжірибеден\s+өтуші|стажер/iu],
30
30
  ];
31
31
  return checks.find(([, pattern]) => pattern.test(text))?.[0] || null;
32
32
  }
@@ -0,0 +1,85 @@
1
+ // Time-sensitive curated data consumed by hiring-safety.js's classification
2
+ // logic. Kept in its own file so updating a blacklist/allowlist never means
3
+ // touching the (stable) regex classification code, and vice versa.
4
+
5
+ export const SCAM_CONTACTS = [
6
+ ['telegram:valery_hr_36', /(?:^|[^a-z0-9_])@?valery_hr_36(?:$|[^a-z0-9_])/i],
7
+ ['telegram:kris_mogelevich7', /(?:^|[^a-z0-9_])@?kris_mogelevich7(?:$|[^a-z0-9_])/i],
8
+ ['telegram:gasgazz_07', /(?:^|[^a-z0-9_])@?gasgazz_07(?:$|[^a-z0-9_])/i],
9
+ ['phone:+998992993435', /(?:\+?998[\s()-]*)99[\s()-]*299[\s()-]*34[\s()-]*35/],
10
+ ['phone:+998992600344', /(?:\+?998[\s()-]*)99[\s()-]*260[\s()-]*03[\s()-]*44/],
11
+ ['phone:+998931244802', /(?:\+?998[\s()-]*)93[\s()-]*124[\s()-]*48[\s()-]*02/],
12
+ ];
13
+
14
+ // Exact Telegram handles from reviewed external blacklists. User comments / "please
15
+ // check" submissions are deliberately excluded; only the editorial/list body is used.
16
+ export const REPORTED_TELEGRAM_BY_SOURCE = {
17
+ moshelovka: [
18
+ 'pitupishka', 'obnalmanua1', 'p2p_lab_processing', 'hoodmoneyp2p', 'p2prvt',
19
+ 'protsessing', 'protsessing0', 'dropovod01k_chat', 'processing_skupka', 'mamonts',
20
+ 'brown_bear0', 'mediap2p', 'proseccina', 'amanatniy', 'pitupitradersrf',
21
+ 'russiantradersclubs', 'vvaybit',
22
+ ],
23
+ vklader: [
24
+ 'dobro_ot_yana', 'senpaj_help', 'shinobi_help', 'daime_helper', 'ninja_inform',
25
+ 'ninja_invest', 'assistent_ninja', 'alex_resolution', 'alex_crypto_way',
26
+ 'joecopytrade_bot', 'kirill_onchain', 'cryptolnspect', 'ghostl1', 'slashl1',
27
+ 'alexandrzenin', 'seriy_crypt', 'airolejon', 'olekitka', 'poizonrider',
28
+ 'poizonriderrobot', 'riderfeedback', 'poizonridersupport', 'poizondaniel',
29
+ 'poizonfenix', 'poizonsector', 'poizonlevel', 'poizonline', 'poizongo',
30
+ 'poizonoffice', 'poizonnation', 'poizonstorm', 'poizonsystem', 'poizonaura',
31
+ 'islyam_t', 'ethio_adam', 'vitalikadminn', 'ilya_vias', 'taddypedy', 'happyroman',
32
+ 'maxhappyict', 'rrrrviprr_bot', 'artem1991v', 'samuray_new', 'gen_zemtsov',
33
+ 'shortist_owner', 'arbitrage_capital', 'vladimir_arbitrage', 'mkwaydq',
34
+ 'poslednii_chance', 'romantradee', 'white_voronbtc', 'white_crow_btc',
35
+ 'unilive_network', 'potokcash', 'cashflowfund', 'potokpoint', 'cash_potok',
36
+ 'cashflowtime', 'kate_559', 'capitalforward', 'allocation_ay', 'andraicrypto',
37
+ 'andraicrypto_manager', 'vladbelokrylov', 'mersedes1_1', 'vadim_hub', 'david_gg7',
38
+ 'konstantinpravda', 'marafondeadinrich_bot', 'maratwhale', 'marat_whale',
39
+ 'fadeev_trade', 'andreysrbrv', 'speculant_g', 'sniperusdt', 'ratner_official',
40
+ 'arthurratner', 'markglavnyy', 'savivoin', 'l1r1q', 'arturomega', 'rodioncrypt0',
41
+ 'vipbyrodion_bot', 'magistr_tr', 'maxcrypto_adm', 'snipervip0001_bot', 'denis_longist',
42
+ 'alexeyaltador', 'anton_manag', 'twotradeowner', 'alexey_maker', 'alex_profitmaker',
43
+ 'hivetrader', 'robert_crypto98', 'neesmshnyi_bot', 'crypto_compass_btc', 'ska1pgod',
44
+ 'ternov_alexi', 'ternovhellobot', 'rafael_markov', 'cap_scalperr', 'dmitrukotov',
45
+ 'dmitrukot', 'crypto_partners_bot', 'learnarb_crypto', 'alinnainvest', 'speculyantt',
46
+ 'alex1trader', 'alexodessa_invest', 'alexodessa', 'maxbrotrd', 'mbro_pocket',
47
+ 'sergosnova', 'seedwalletshop_bot', 'seedpkultrasbot', 'arbitrageeproc', 'lebed_off',
48
+ 'cryptobotarbitrage_bot', 'youmentor_anna', 'kowalrenata', 'lunosupportstradebot',
49
+ 'nikicrypto_stre', 'exmonftmarket_bot', 'pr1vatee_roman', 'roman_pr1vat', 'rich_dmitry',
50
+ 'rudi_dmitry', 'sd_0986_bot', 'igor_richman', 'alex_wise_trade', 'alex_wiseman',
51
+ 'brokertribunai', 'tradelab_bot', 'tradelab_channel', 'tradelab_community',
52
+ 'superrare_thebot', 'traderr_server', 'trader_serverr', 'trader_servers',
53
+ 'traderer_stock', 'tanyamikheeva_pro_dengi', 'tanyamikheeva', 'veraastroguide',
54
+ 'daryu_money', 'mur_anastasi_official', 'dengisvoim_bot', 'minaevosnova',
55
+ 'pumpdumpcrypto_bot', 'trader_servver', 'traderr_serverl', 'maximonchain',
56
+ 'crypro_objectt', 'litvinov_teach', 'andrey_onchain', 'bank_tbx_bot', 'crypto_objectt',
57
+ ],
58
+ };
59
+
60
+ export const REPORTED_TELEGRAM_INVITES = [
61
+ ['moshelovka:invite-hqnj', 't.me/+-hqnjkgsgha2zwm0'],
62
+ ['moshelovka:invite-adhp', 't.me/+adhpbvvjddk2njfi'],
63
+ ['vklader:penguin-protocol', 't.me/+jqffw2xgl04wytux'],
64
+ ['vklader:velvethaze', 't.me/joinchat/57q3oqqv_ea2njhi'],
65
+ ['vklader:pump', 't.me/+6ddntu5hsuo3ntmy'],
66
+ ['vklader:bullvault', 't.me/joinchat/5ll_t_cthgbhytri'],
67
+ ['vklader:lumencap', 't.me/joinchat/dbmlm5ieytezmwji'],
68
+ ['vklader:cryptoosnova', 't.me/joinchat/zylzt2q8gmc1mjyy'],
69
+ ];
70
+
71
+ // Current licensed private employment agencies in Uzbekistan.
72
+ // Snapshot of the official Migration Agency register, updated there on 2026-08-08.
73
+ // Absence is a SOFT signal only; direct foreign employers are not intermediaries.
74
+ export const UZ_LICENSED_FOREIGN_EMPLOYMENT_AGENCIES = [
75
+ 'reiwa', 'migo overseas consulting', 'turon world cooperation', 'specialist group',
76
+ 'naimix', 'common sense', 'dhaef global', 'job maker', 'the best-staff', 'mir power',
77
+ 'horizon work', 'world wide immigration', 'the kasb', 'best globalize',
78
+ 'gotalent international', 'globalhr', 'international migration line', 'work expert',
79
+ 'viza master', 'diamor', 'fairness japan', 'getwork', 'garant immigration',
80
+ 'youth globe', 'jobbridge', 'aimjob', 'worknet', 'gilen jobs', 'interwork', 'oukaway',
81
+ 'bestwill', 'world bridge', 'gate', 'immigration service', 'visacentrum',
82
+ 'resurs export group', 'imkon', 'trust migration', 'united careers', 'talantum group',
83
+ 'skd man power', 'qadam global', 'workline', 'meros job', 'hr job start',
84
+ 'mora work group', 'jobex',
85
+ ];
@@ -1,6 +1,13 @@
1
1
  // Vacancy risk + vagueness classification.
2
2
  // Hard-block only high-confidence industry/scam patterns; ambiguous signals stay soft.
3
3
 
4
+ import {
5
+ SCAM_CONTACTS,
6
+ REPORTED_TELEGRAM_BY_SOURCE,
7
+ REPORTED_TELEGRAM_INVITES,
8
+ UZ_LICENSED_FOREIGN_EMPLOYMENT_AGENCIES,
9
+ } from './hiring-safety-blacklist.js';
10
+
4
11
  // ---- Hard-blocked: gambling / iGaming -------------------------------------
5
12
  const GAMBLING = [
6
13
  ['casino', /\bcasino\b|казино|казіно|kazino/i],
@@ -30,72 +37,6 @@ const SCAM = [
30
37
  ['crypto-bait', /гарантированн[а-яёіїєґ]*\s+(?:прибыл|профит)|трейдинг\s+с\s+гарант|инвестиц[а-яёіїєґ]*\s+с\s+гарант/i],
31
38
  ];
32
39
 
33
- const SCAM_CONTACTS = [
34
- ['telegram:valery_hr_36', /(?:^|[^a-z0-9_])@?valery_hr_36(?:$|[^a-z0-9_])/i],
35
- ['telegram:kris_mogelevich7', /(?:^|[^a-z0-9_])@?kris_mogelevich7(?:$|[^a-z0-9_])/i],
36
- ['telegram:gasgazz_07', /(?:^|[^a-z0-9_])@?gasgazz_07(?:$|[^a-z0-9_])/i],
37
- ['phone:+998992993435', /(?:\+?998[\s()-]*)99[\s()-]*299[\s()-]*34[\s()-]*35/],
38
- ['phone:+998992600344', /(?:\+?998[\s()-]*)99[\s()-]*260[\s()-]*03[\s()-]*44/],
39
- ['phone:+998931244802', /(?:\+?998[\s()-]*)93[\s()-]*124[\s()-]*48[\s()-]*02/],
40
- ];
41
-
42
- // Exact Telegram handles from reviewed external blacklists. User comments / "please
43
- // check" submissions are deliberately excluded; only the editorial/list body is used.
44
- const REPORTED_TELEGRAM_BY_SOURCE = {
45
- moshelovka: [
46
- 'pitupishka', 'obnalmanua1', 'p2p_lab_processing', 'hoodmoneyp2p', 'p2prvt',
47
- 'protsessing', 'protsessing0', 'dropovod01k_chat', 'processing_skupka', 'mamonts',
48
- 'brown_bear0', 'mediap2p', 'proseccina', 'amanatniy', 'pitupitradersrf',
49
- 'russiantradersclubs', 'vvaybit',
50
- ],
51
- vklader: [
52
- 'dobro_ot_yana', 'senpaj_help', 'shinobi_help', 'daime_helper', 'ninja_inform',
53
- 'ninja_invest', 'assistent_ninja', 'alex_resolution', 'alex_crypto_way',
54
- 'joecopytrade_bot', 'kirill_onchain', 'cryptolnspect', 'ghostl1', 'slashl1',
55
- 'alexandrzenin', 'seriy_crypt', 'airolejon', 'olekitka', 'poizonrider',
56
- 'poizonriderrobot', 'riderfeedback', 'poizonridersupport', 'poizondaniel',
57
- 'poizonfenix', 'poizonsector', 'poizonlevel', 'poizonline', 'poizongo',
58
- 'poizonoffice', 'poizonnation', 'poizonstorm', 'poizonsystem', 'poizonaura',
59
- 'islyam_t', 'ethio_adam', 'vitalikadminn', 'ilya_vias', 'taddypedy', 'happyroman',
60
- 'maxhappyict', 'rrrrviprr_bot', 'artem1991v', 'samuray_new', 'gen_zemtsov',
61
- 'shortist_owner', 'arbitrage_capital', 'vladimir_arbitrage', 'mkwaydq',
62
- 'poslednii_chance', 'romantradee', 'white_voronbtc', 'white_crow_btc',
63
- 'unilive_network', 'potokcash', 'cashflowfund', 'potokpoint', 'cash_potok',
64
- 'cashflowtime', 'kate_559', 'capitalforward', 'allocation_ay', 'andraicrypto',
65
- 'andraicrypto_manager', 'vladbelokrylov', 'mersedes1_1', 'vadim_hub', 'david_gg7',
66
- 'konstantinpravda', 'marafondeadinrich_bot', 'maratwhale', 'marat_whale',
67
- 'fadeev_trade', 'andreysrbrv', 'speculant_g', 'sniperusdt', 'ratner_official',
68
- 'arthurratner', 'markglavnyy', 'savivoin', 'l1r1q', 'arturomega', 'rodioncrypt0',
69
- 'vipbyrodion_bot', 'magistr_tr', 'maxcrypto_adm', 'snipervip0001_bot', 'denis_longist',
70
- 'alexeyaltador', 'anton_manag', 'twotradeowner', 'alexey_maker', 'alex_profitmaker',
71
- 'hivetrader', 'robert_crypto98', 'neesmshnyi_bot', 'crypto_compass_btc', 'ska1pgod',
72
- 'ternov_alexi', 'ternovhellobot', 'rafael_markov', 'cap_scalperr', 'dmitrukotov',
73
- 'dmitrukot', 'crypto_partners_bot', 'learnarb_crypto', 'alinnainvest', 'speculyantt',
74
- 'alex1trader', 'alexodessa_invest', 'alexodessa', 'maxbrotrd', 'mbro_pocket',
75
- 'sergosnova', 'seedwalletshop_bot', 'seedpkultrasbot', 'arbitrageeproc', 'lebed_off',
76
- 'cryptobotarbitrage_bot', 'youmentor_anna', 'kowalrenata', 'lunosupportstradebot',
77
- 'nikicrypto_stre', 'exmonftmarket_bot', 'pr1vatee_roman', 'roman_pr1vat', 'rich_dmitry',
78
- 'rudi_dmitry', 'sd_0986_bot', 'igor_richman', 'alex_wise_trade', 'alex_wiseman',
79
- 'brokertribunai', 'tradelab_bot', 'tradelab_channel', 'tradelab_community',
80
- 'superrare_thebot', 'traderr_server', 'trader_serverr', 'trader_servers',
81
- 'traderer_stock', 'tanyamikheeva_pro_dengi', 'tanyamikheeva', 'veraastroguide',
82
- 'daryu_money', 'mur_anastasi_official', 'dengisvoim_bot', 'minaevosnova',
83
- 'pumpdumpcrypto_bot', 'trader_servver', 'traderr_serverl', 'maximonchain',
84
- 'crypro_objectt', 'litvinov_teach', 'andrey_onchain', 'bank_tbx_bot', 'crypto_objectt',
85
- ],
86
- };
87
-
88
- const REPORTED_TELEGRAM_INVITES = [
89
- ['moshelovka:invite-hqnj', 't.me/+-hqnjkgsgha2zwm0'],
90
- ['moshelovka:invite-adhp', 't.me/+adhpbvvjddk2njfi'],
91
- ['vklader:penguin-protocol', 't.me/+jqffw2xgl04wytux'],
92
- ['vklader:velvethaze', 't.me/joinchat/57q3oqqv_ea2njhi'],
93
- ['vklader:pump', 't.me/+6ddntu5hsuo3ntmy'],
94
- ['vklader:bullvault', 't.me/joinchat/5ll_t_cthgbhytri'],
95
- ['vklader:lumencap', 't.me/joinchat/dbmlm5ieytezmwji'],
96
- ['vklader:cryptoosnova', 't.me/joinchat/zylzt2q8gmc1mjyy'],
97
- ];
98
-
99
40
  const REPORTED_TELEGRAM_INDEX = (() => {
100
41
  const index = new Map();
101
42
  for (const [source, handles] of Object.entries(REPORTED_TELEGRAM_BY_SOURCE)) {
@@ -163,22 +104,6 @@ const FOREIGN_JOB_SIGNAL = /работ[а-яёіїєґ]*\s+(?:за\s+грани
163
104
  const FOREIGN_EMPLOYMENT_INTERMEDIARY = /агентств[а-яёіїєґ]*\s+(?:по\s+)?(?:трудоустройств|занятост)|посредник[а-яёіїєґ]*\s+(?:по\s+)?трудоустройств|подбор\s+работ[а-яёіїєґ]*\s+за\s+(?:границ|рубеж)|помощ[а-яёіїєґ]*\s+(?:с\s+)?(?:трудоустройств|оформлени[а-яёіїєґ]*\s+(?:рабоч[а-яёіїєґ]*\s+)?виз)|employment\s+agency|recruitment\s+agency/i;
164
105
  const FOREIGN_JOB_UPFRONT_FEE = /(?:работ[а-яёіїєґ]*\s+(?:за\s+границ|за\s+кордон|за\s+рубеж)|трудоустройств[а-яёіїєґ]*\s+(?:за\s+границ|за\s+рубеж)|працевлаштуван[а-яёіїєґ]*\s+за\s+кордон)[\s\S]{0,600}(?:предоплат|внести|оплатить|перевести)[^.\n]{0,140}(?:виз|приглашени|запрошенн|документ|страхов|регистрац|бронь|гарантийн[а-яёіїєґ]*\s+взнос|услуг[а-яёіїєґ]*\s+агентств)/i;
165
106
 
166
- // Current licensed private employment agencies in Uzbekistan.
167
- // Snapshot of the official Migration Agency register, updated there on 2026-08-08.
168
- // Absence is a SOFT signal only; direct foreign employers are not intermediaries.
169
- const UZ_LICENSED_FOREIGN_EMPLOYMENT_AGENCIES = [
170
- 'reiwa', 'migo overseas consulting', 'turon world cooperation', 'specialist group',
171
- 'naimix', 'common sense', 'dhaef global', 'job maker', 'the best-staff', 'mir power',
172
- 'horizon work', 'world wide immigration', 'the kasb', 'best globalize',
173
- 'gotalent international', 'globalhr', 'international migration line', 'work expert',
174
- 'viza master', 'diamor', 'fairness japan', 'getwork', 'garant immigration',
175
- 'youth globe', 'jobbridge', 'aimjob', 'worknet', 'gilen jobs', 'interwork', 'oukaway',
176
- 'bestwill', 'world bridge', 'gate', 'immigration service', 'visacentrum',
177
- 'resurs export group', 'imkon', 'trust migration', 'united careers', 'talantum group',
178
- 'skd man power', 'qadam global', 'workline', 'meros job', 'hr job start',
179
- 'mora work group', 'jobex',
180
- ];
181
-
182
107
  function normalizeCompany(value) {
183
108
  return value
184
109
  .toLowerCase()
@@ -111,7 +111,11 @@ export function parseHiringSalaryWithContext(value, options = {}) {
111
111
  return withContext(parseSalary(value), value, options);
112
112
  }
113
113
 
114
- const VACANCY_COMPENSATION_RE = /(?:salary|salary\s+range|base\s+pay|pay\s+range|annual\s+pay|compensation(?:\s+range)?|заработн\p{L}*\s+плат\p{L}*|зарплат\p{L}*|з\s*[\/\\.\-]?\s*п(?=$|[^\p{L}\p{N}_])|оклад\p{L}*|вилка\s+оплат\p{L}*|оплата\s+труда|компенсац\p{L}*|ставка|💵|💰)/giu;
114
+ // Include short local-board labels/markers used in real vacancy feeds: `З/п`,
115
+ // `ЗП`, and the money emoji that ish-bor-style cards use before a salary field.
116
+ // The marker is still required for long prose so unrelated large numbers are not
117
+ // promoted to compensation merely because the vacancy happens to be in UZ/KZ.
118
+ const VACANCY_COMPENSATION_RE = /(?:💵|💰|salary|salary\s+range|base\s+pay|pay\s+range|annual\s+pay|compensation(?:\s+range)?|з\s*[/\\.\-]?\s*п\b|заработн\p{L}*\s+плат\p{L}*|зарплат\p{L}*|оклад\p{L}*|вилка\s+оплат\p{L}*|оплата\s+труда|компенсац\p{L}*|ставка)/giu;
115
119
 
116
120
  // AI-recruiting/staffing postings (e.g. Mercor) routinely mention funding,
117
121
  // valuation or revenue figures in the same listing as the actual salary. A
@@ -141,4 +145,4 @@ export function parseHiringVacancySalary(value, options = {}) {
141
145
  if (parsed && (parsed.min != null || parsed.max != null)) return parsed;
142
146
  }
143
147
  return null;
144
- }
148
+ }
@@ -9,6 +9,10 @@ export const HIRING_MONTHS = Object.freeze({
9
9
  iulie: 6, august: 7, septembrie: 8, octombrie: 9, noiembrie: 10, decembrie: 11,
10
10
  січня: 0, лютого: 1, березня: 2, квітня: 3, травня: 4, червня: 5,
11
11
  липня: 6, серпня: 7, вересня: 8, жовтня: 9, листопада: 10, грудня: 11,
12
+ yanvar: 0, fevral: 1, mart: 2, aprel: 3, iyun: 5,
13
+ iyul: 6, avgust: 7, sentabr: 8, oktabr: 9, noyabr: 10, dekabr: 11,
14
+ қаңтар: 0, ақпан: 1, наурыз: 2, сәуір: 3, мамыр: 4, маусым: 5,
15
+ шілде: 6, тамыз: 7, қыркүйек: 8, қазан: 9, қараша: 10, желтоқсан: 11,
12
16
  });
13
17
 
14
18
  export const UNICODE_LEFT_BOUNDARY = '(?<![\\p{L}\\p{N}])';
@@ -8,7 +8,7 @@ const ADDRESS_LABEL_RE = /(?:адрес|адреса|адресація|адре
8
8
  const PREFIX_STREET_MARKER = String.raw`(?:(?:ул(?:ица)?|вул(?:иця)?|пр|просп(?:ект)?|пр-т|переул(?:ок)?|пров(?:улок)?|проезд|наб(?:ережная)?|шоссе|str(?:ada)?|street|st|avenue|ave|road|rd|көше)\.?)`;
9
9
  const POSTFIX_STREET_MARKER = String.raw`(?:ko['’ʼ\u02bc]?cha(?:si)?|кўча(?:си)?|коча(?:си)?|kocha(?:si)?)`;
10
10
  const POSTFIX_STREET_TYPE = String.raw`(?:вулиця|улица|провулок|переулок|проспект|бульвар|набережна|набережная|шосе|шоссе|площа|площадь|узвіз|спуск|алея|аллея|дорога|тупик)`;
11
- const HOUSE_MARKER = String.raw`(?:дом|д\.|будинок|буд\.|house|h\.|uy|уй|nr\.?|no\.?|№)`;
11
+ const HOUSE_MARKER = String.raw`(?:дом|д\.|будинок|буд\.|house|h\.|uy|уй|үй|nr\.?|no\.?|№)`;
12
12
  const BUILDING_MARKER = String.raw`(?:корп(?:ус)?\.?|к\.|строен(?:ие)?|стр\.|будова|секц(?:ия|ія)?|bloc|corp|building|bldg\.?|korpus)`;
13
13
  const NUMBER_TOKEN = String.raw`\d{1,5}(?:[-\/]?[\p{L}])?(?:[\/-]\d{1,4}(?:[-\/]?[\p{L}])?)?`;
14
14
  const STREET_WORD = String.raw`[\p{L}'’.-]{2,48}`;
@@ -21,6 +21,15 @@ export function looksParkingOnly(value) {
21
21
  }
22
22
 
23
23
  // Cross-language "no agency" / "direct from owner" signals.
24
+ //
25
+ // This is a separate, deliberately broader/stricter pattern than
26
+ // SELLER_TERMS.owner (housing.js) / parseHousingSeller (housing-structured.js)
27
+ // — not an accidental duplicate. Consumers use isDirectOwner as a
28
+ // high-confidence override checked BEFORE falling back to
29
+ // parseHousingSeller's owner-vs-agency comparative scoring (see
30
+ // classifyTelegramAgency in whiteslove.me-backend-platform's
31
+ // apps/flats/src/scrapers/telegram.js). If you're tempted to merge these,
32
+ // check that call site's expectations first.
24
33
  const DIRECT_OWNER_RE = /(?:без\s+(?:макл(?:ер[а-яё]*)?|посредник[а-яё]*|ри[еэ]?лтор[а-яё]*|агент[а-яё]*)|от\s+(?:собственник[а-яё]*|хозяин[а-яё]*)|від\s+(?:власник[а-яіїґ]*|власниц[яії][а-яіїґ]*|господар[а-яіїґ]*)|без\s+(?:посередник[а-яіїґ]*|рі[єе]лтор[а-яіїґ]*|агент[а-яіїґ]*)|прямо\s+від\s+(?:власник[а-яіїґ]*|власниц[яії][а-яіїґ]*|господар[а-яіїґ]*)|(?:власник|власниця)\s+(?:зда[єе]|прода[єе])|no\s+(?:agency|broker|realtor|agent)|owner\s+direct|direct\s+from\s+(?:owner|landlord)|f(?:ă|a)r(?:ă|a)\s+(?:agen(?:ț|t)ie|intermediar\w*)|direct\s+(?:de\s+la\s+)?proprietar|makler\s*[- ]?siz|maklersiz|bez\s*makler(?:a|ov)?|bezmakler(?:a|ov)?|vositachi\s*[- ]?siz|vositachisiz|egasidan|uy\s+egasidan|делдалсыз|делдал\s*жоқ|иесінен|үй\s+иесінен)/iu;
25
34
 
26
35
  // Cross-language "no commission" signals. Direct-owner language implies zero
@@ -24,10 +24,23 @@ const DISPLAY = Object.freeze({
24
24
  'Bobur Park': 'Парк Бобура', 'Independence Square': 'Площадь Независимости', 'Mega Planet': 'Mega Planet',
25
25
  'Bek Baraka': 'Бек-Барака', Khadra: 'Хадра',
26
26
  }),
27
+ uk: Object.freeze({
28
+ Park: 'Парк', Metro: 'Метро', 'Bus stop': 'Автобусна зупинка', 'Public transport': 'Громадський транспорт',
29
+ 'Main road': 'Головна дорога', Clinic: 'Клініка', 'Maternity hospital': 'Пологовий будинок', Hospital: 'Лікарня',
30
+ School: 'Школа', Kindergarten: 'Дитячий садок', Childcare: 'Дитячі заклади', University: 'Університет',
31
+ 'Shopping center': 'Торговий центр', Shop: 'Магазин', Korzinka: 'Korzinka', Supermarket: 'Супермаркет',
32
+ Market: 'Ринок', Cafe: 'Кафе', Restaurant: 'Ресторан', Playground: 'Дитячий майданчик', Pharmacy: 'Аптека',
33
+ Mosque: 'Мечеть', Church: 'Церква', 'Railway station': 'Залізничний вокзал', Airport: 'Аеропорт',
34
+ 'Bobur Park': 'Парк Бабура', 'Independence Square': 'Площа Незалежності', 'Mega Planet': 'Mega Planet',
35
+ 'Bek Baraka': 'Бек-Барака', Khadra: 'Хадра',
36
+ }),
27
37
  });
28
38
 
29
39
  function languageKey(locale) {
30
- return String(locale || 'en').toLowerCase().startsWith('ru') ? 'ru' : 'en';
40
+ const key = String(locale || 'en').toLowerCase();
41
+ if (key.startsWith('uk')) return 'uk';
42
+ if (key.startsWith('ru')) return 'ru';
43
+ return 'en';
31
44
  }
32
45
 
33
46
  function exactPoi(value) {
@@ -26,14 +26,14 @@ const APPLIANCE_PATTERNS = Object.freeze([
26
26
  ]);
27
27
 
28
28
  const FIRST_RENT_UZ_RE = /(?:hali\s+hech\s+kim\s+(?:yashamagan|turmagan)|ҳали\s+ҳеч\s+ким\s+(?:яшамаган|турмаган))/iu;
29
- const LANDLORD_PRESENT_RE = /(?:xozaykali|hojaykali|xo['’]?jaykali|с\s+хозяйк(?:ой|ой\s+в\s+квартире)|хозяйк\p{L}*\s+(?:жив[её]т|прожива\p{L}*))/iu;
30
- const STUDENT_RE = /(?:studentlar\s+uchun|talabalar\s+uchun|студент(?:ам|ы|ок|ов)?\s+(?:можно|для)|для\s+студент)/iu;
31
- const NO_BROKER_RE = /(?:bez\s+makler|maklersiz|vositachisiz|без\s+(?:маклер|посредник|риелтор|риэлтор|комисси))/iu;
32
- const BROKER_RE = /(?:makler|vositachi|макл(?:ер[а-яё]*)?|ри[еэ]лтор[а-яё]*|агентств[а-яё]*|комисси[а-яё]*|broker|realtor|commission)/iu;
33
- const MEN_RE = /(?:o['’ʻʼ‘`]?g['’ʻʼ‘`]?il\s+bola(?:lar)?(?:ga)?|ogil\s+bola(?:lar)?(?:ga)?|sherik\s+bola|эркак(?:лар)?|erkak(?:lar)?(?:ga)?|только\s+(?:мужчин|парн))/iu;
34
- const WOMEN_RE = /(?:qiz(?:lar)?(?:ga)?|ayol(?:lar)?(?:ga|ni)?|киз(?:лар)?(?:га)?|аёл(?:лар)?(?:га|ни)?|девушк\p{L}*|женщин\p{L}*|girls?\s+only|women\s+only)/iu;
35
- const FAMILY_RE = /(?:семь\p{L}*|family|oila(?:ga|lar|li)?|oila\s+uchun|оилага|оелага|оилавий|oelaga)/iu;
36
- const ROOM_SHARE_RE = /(?:sherik(?:ka|lik|likga)?|шерик(?:ка|лик)?|roommate|flatmate|подселени|койко[-\s]?мест|место\s+в\s+(?:комнат|квартир)|birga\s+yashash(?:ga)?|kvartira(?:ga|da)?[^\r\n.!?]{0,36}(?:\d+|bitta|1)\s*(?:ta\s*)?(?:qiz|ayol)[^\r\n.!?]{0,20}(?:ijarachi\s*)?(?:kerak|kere))/iu;
29
+ const LANDLORD_PRESENT_RE = /(?:xozaykali|hojaykali|xo['’]?jaykali|с\s+хозяйк(?:ой|ой\s+в\s+квартире)|хозяйк\p{L}*\s+(?:жив[её]т|прожива\p{L}*)|with\s+(?:the\s+)?(?:landlord|owner)\s+(?:present|living\s+in)|cu\s+proprietar(?:ul)?\s+în\s+cas(?:ă|a)|үй\s*иесі\s+(?:тұрады|бірге\s+тұрады))/iu;
30
+ const STUDENT_RE = /(?:studentlar\s+uchun|talabalar\s+uchun|студент(?:ам|ы|ок|ов)?\s+(?:можно|для)|для\s+студент|students?\s+(?:only|welcome)|for\s+students|pentru\s+studen[țt]i|studen[țt]i(?:lor)?|студенттерге|студенттер\s+үшін)/iu;
31
+ const NO_BROKER_RE = /(?:bez\s+makler|maklersiz|vositachisiz|без\s+(?:маклер|посредник|риелтор|риэлтор|комисси)|no\s+(?:broker|agent|commission|agency\s+fee)|f[ăa]r[ăa]\s+(?:comision|agen[țt]ie|intermediari)|делдалсыз|комиссиясыз)/iu;
32
+ const BROKER_RE = /(?:makler|vositachi|макл(?:ер[а-яё]*)?|ри[еэ]лтор[а-яё]*|агентств[а-яё]*|комисси[а-яё]*|broker|realtor|commission|comision(?:ul)?|agen[țt]ie|delda[lл]\p{L}*|делдал\p{L}*)/iu;
33
+ const MEN_RE = /(?:o['’ʻʼ‘`]?g['’ʻʼ‘`]?il\s+bola(?:lar)?(?:ga)?|ogil\s+bola(?:lar)?(?:ga)?|sherik\s+bola|эркак(?:лар)?|erkak(?:lar)?(?:ga)?|только\s+(?:мужчин|парн)|\bmen\s+only\b|b[ăa]rba[țt]i(?:lor)?|b[ăa]ie[țt]i(?:lor)?|жігіттерге|жігіттер(?:ге)?|хлопц(?:ям|і|ів)?|чоловік(?:ам|и)?)/iu;
34
+ const WOMEN_RE = /(?:qiz(?:lar)?(?:ga)?|ayol(?:lar)?(?:ga|ni)?|киз(?:лар)?(?:га)?|аёл(?:лар)?(?:га|ни)?|девушк\p{L}*|женщин\p{L}*|girls?\s+only|women\s+only|fete(?:lor)?|femei(?:lor)?|қыздарға|қыздар(?:ға)?|дівчат(?:ам|а|ок)?|жінк(?:ам|и)?)/iu;
35
+ const FAMILY_RE = /(?:семь\p{L}*|family|oila(?:ga|lar|li)?|oila\s+uchun|оилага|оелага|оилавий|oelaga|famil(?:ie|ia)|cuplu(?:ri)?|отбасына|отбасылы|жанұяға|сім['’ʼ]?[яїі](?:ям|ям[иі])?|сімейн\p{L}*)/iu;
36
+ const ROOM_SHARE_RE = /(?:sherik(?:ka|lik|likga)?|шерик(?:ка|лик)?|roommate|flatmate|подселени|койко[-\s]?мест|место\s+в\s+(?:комнат|квартир)|birga\s+yashash(?:ga)?|kvartira(?:ga|da)?[^\r\n.!?]{0,36}(?:\d+|bitta|1)\s*(?:ta\s*)?(?:qiz|ayol)[^\r\n.!?]{0,20}(?:ijarachi\s*)?(?:kerak|kere)|coleg\s+de\s+(?:apartament|camer[ăa])|bed\s+space|бөлмелес(?:\s+керек)?|көрші\s+керек)/iu;
37
37
  const AIR_CONDITIONER_RE = /(?:кондицион|air\s*con|konditsioner|kandit(?:s|c)?aner|kanditsaner|кандитсанер)/iu;
38
38
  const PER_PERSON_PRICE_RE = /(?:kishi\s+boshiga|киши\s+бошига)\s*(\d{1,3}(?:[\s.,]\d{3})*|\d+(?:[.,]\d+)?)\s*(ming|минг|million|mln|млн)?(?:dan|дан)?/iu;
39
39
  const WALK_MINUTES_RE = /(?:yayov|piyoda|пешком)\s*(\d{1,2})\s*(?:daqiqa|min(?:ute)?s?|минут)/iu;
@@ -271,4 +271,4 @@ export function parseHousingPrice(value, fallbackCurrency = '') {
271
271
  }
272
272
 
273
273
  // Compatibility name for callers migrating from Flat Finder's local parser.
274
- export const parsePriceFromText = parseHousingPrice;
274
+ export const parsePriceFromText = parseHousingPrice;
@@ -10,6 +10,7 @@ export const HOUSING_DEAL_TYPE_EXTENSIONS = Object.freeze([
10
10
  group('longRent', {
11
11
  ru: ['снять', 'в месяц', 'месяц'],
12
12
  en: ['monthly'],
13
+ uk: ['зняти', 'на місяць', 'місяць'],
13
14
  uzLatn: ['ijaraga', 'oyiga', 'beriladi'],
14
15
  uzCyrl: ['ижарага', 'ойига', 'берилади'],
15
16
  kk: ['айына'],
@@ -17,6 +18,7 @@ export const HOUSING_DEAL_TYPE_EXTENSIONS = Object.freeze([
17
18
  group('shortRent', {
18
19
  ru: ['сутки', 'суток', 'суточно'],
19
20
  en: ['daily'],
21
+ uk: ['доба', 'діб', 'подобово'],
20
22
  uzLatn: ['kunlik', 'sutkalik'],
21
23
  uzCyrl: ['кунлик', 'суткалик'],
22
24
  kk: ['тәуліктік'],
@@ -27,6 +29,7 @@ export const HOUSING_ROOM_ONLY_EXTENSIONS = Object.freeze([
27
29
  group('roomOnly', {
28
30
  ru: ['шеринг'],
29
31
  en: ['room only'],
32
+ uk: ['шерінг'],
30
33
  uzLatn: ['student qizlarga', 'talaba qizlarga', 'opshijit dom', 'obshijit dom'],
31
34
  uzCyrl: ['студент қизларга', 'талаба қизларга'],
32
35
  }),
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ export * from './location-merge.js';
21
21
  export * from './locations-runtime.js';
22
22
  export * from './kg-location-extensions.js';
23
23
  export * from './kz-location-extensions.js';
24
+ export * from './kz-district-extensions.js';
24
25
  export * from './uz-location-extensions.js';
25
26
  export * from './central-asia-locations.js';
26
27
  export * from './ua-location-extensions-major.js';
@@ -30,6 +31,7 @@ export * from './landmarks.js';
30
31
  export * from './tashkent-colloquial.js';
31
32
  export * from './tashkent-residential-complexes.js';
32
33
  export * from './tashkent-pois.js';
34
+ export * from './tashkent-gastronomic-streets.js';
33
35
  export * from './odesa-metropolitan.js';
34
36
  export * from './housing.js';
35
37
  export * from './housing-intent.js';