@whiteslove/parsing-lexicon 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for Whiteslove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,12 +21,14 @@ export interface SourceRoleNormalization {
21
21
  canonical: string;
22
22
  label: string;
23
23
  re: RegExp;
24
+ labels: readonly string[];
24
25
  }
25
26
 
26
27
  export const SOURCE_PROFESSION_ALIASES: readonly SourceProfessionAlias[];
27
28
  export const SOURCE_ROLE_NORMALIZATION_RULES: readonly SourceRoleNormalization[];
28
29
  export const SOURCE_CANDIDATE_INTENT_ALIASES: readonly string[];
29
30
  export function normalizeSourceRole(value: unknown): Readonly<{ canonical: string; label: string }> | null;
31
+ export function normalizeSourceRoleKeys(value: unknown): readonly string[] | null;
30
32
  export function matchesSourceCandidateIntent(value: unknown): boolean;
31
33
  export function professionDisplayLabel(canonical: string): string;
32
34
  export function matchExtendedProfessions(value: unknown, options?: { limit?: number; allowWeak?: boolean }): readonly ExtendedProfessionMatch[];
@@ -48,7 +48,9 @@ export const SOURCE_PROFESSION_ALIASES = Object.freeze([
48
48
  sourceRole('welder', 'Welder', ['сварщиком', 'сварщица', 'зварювальником'], 'construction'),
49
49
  ]);
50
50
 
51
- const sourceRoleNormalization = (canonical, label, re) => Object.freeze({ canonical, label, re });
51
+ const sourceRoleNormalization = (canonical, label, re, labels) => Object.freeze({
52
+ canonical, label, re, labels: Object.freeze(labels || [label]),
53
+ });
52
54
 
53
55
  /**
54
56
  * Anchored or strongly contextualized raw-role repairs. These rules are kept
@@ -65,7 +67,25 @@ export const SOURCE_ROLE_NORMALIZATION_RULES = Object.freeze([
65
67
  sourceRoleNormalization('sales_manager', 'Sales Manager', /^(?:sales\s+executive(?:\s+ind)?|роп(?:,?\s*sales\s+executive)?)$/iu),
66
68
  sourceRoleNormalization('operative_officer', 'Operative Officer', /^(?:оперативник|оперуполномоченн\p{L}*|оперативный\s+уполномоченн\p{L}*)$/iu),
67
69
  sourceRoleNormalization('water_supply_specialist', 'Water Supply Specialist', /^(?:suv\s+ta['’ʻʼ‘`]?minoti|водоснабжение)$/iu),
68
- sourceRoleNormalization('any_role', 'Any Role', /^(?:onlayn(?:\s+ish(?:chi)?)?|online(?:\s+ish(?:chi)?)?|онлайн|удал[её]нно|remote(?:\s+work)?|boshqa\s+ishlar?|farqi\s+(?:yo['’ʻʼ‘`]?q|yuq)|tungi|bilmaym\p{L}*)$/iu),
70
+ sourceRoleNormalization('any_role', 'Any Role', /^(?:ищу\s+(?:работу|подработку)(?:\s+(?:онлайн|удал[её]нно))?|удал[её]нн\p{L}*\s+подработк\p{L}*(?:\s+за\s+компьютером)?(?:\s+для\s+студентов)?|работа\s+студентам|подработка|работа|работу|любая\s+работа|любая\s+занятость|не\s*важно|без\s+разницы|нет\s+разницы|farqi\s+yo['’ʻʼ‘`]?q|farqi\s+yuq|boshqa\s+ishlar?|ish|ish\s+kerak|ish\s+qidir(?:yapman|aman)|ish\s+izlayapman|onlayn(?:\s+ish(?:chi)?)?|online(?:\s+ish(?:chi)?)?|онлайн|удал[её]нно|remote(?:\s+work)?|tungi|uyda|ofisda|bilmaym\p{L}*|noma['’ʻʼ‘`]?lum(?:\s+\p{L}+)?|ba|va)$/iu),
71
+ sourceRoleNormalization('lawyer_teacher', 'Lawyer', /huquqshunos[^\n,;]*(?:,|\/|\s)+(?:pedagog|o['’ʻʼ‘`]?qituvchi)|pedagog[^\n,;]*(?:,|\/|\s)+huquqshunos/iu, ['Lawyer', 'Teacher']),
72
+ sourceRoleNormalization('erp_administrator_analyst', 'ERP Administrator', /^erp\s+administrator\p{L}*\s*(?:&|,|\/|va)\s*data\s+tahlilchi$/iu, ['ERP Administrator', 'Analyst']),
73
+ sourceRoleNormalization('translator_operator', 'Translator', /tarjimon\p{L}*[^\n]*(?:operator|data\s+otish)|operator\p{L}*[^\n]*tarjimon/iu, ['Translator', 'Operator']),
74
+ sourceRoleNormalization('lawyer', 'Lawyer', /^(?:yurisprudensiya\s+)?huquq(?:shunos)?[^\n]*|^yur(?:isprudensiya|ist)[^\n]*$/iu),
75
+ sourceRoleNormalization('teacher', 'Teacher', /^(?:matematika\s+)?o['’ʻʼ‘`]?qituvchi(?:lik)?$/iu),
76
+ sourceRoleNormalization('finance_banking_specialist', 'Finance / Banking Specialist', /^kredit\s+bo['’ʻʼ‘`]?yicha\s+mutaxa(?:s|ss)is$/iu),
77
+ sourceRoleNormalization('finance_banking_specialist', 'Finance / Banking Specialist', /^(?:финансы?\s*[,/&+]\s*банки?|банки?\s*[,/&+]\s*финансы?|finance\s*[,/&+]\s*banking)$/iu),
78
+ sourceRoleNormalization('welder', 'Welder', /^(?:svarchik|svarshik)$/iu),
79
+ sourceRoleNormalization('factory_worker', 'Factory Worker', /^(?:jizzax\s+)?kia\s+zavodidan\s+ish\s+kerak$|^zavod\s+ishlari(?:\s+.*)?$/iu),
80
+ sourceRoleNormalization('it_specialist', 'IT Specialist', /^(?:kompyuter\s+(?:sohasida|xizmatlari\s+bo['’ʻʼ‘`]?yicha\s+ish\s+kerak)|it\s+kompyuter)$/iu),
81
+ sourceRoleNormalization('restaurant_manager', 'Restaurant Manager', /^(?:restoran|restaurant)[^\n]*(?:boshqaruv|manager|menejer)/iu),
82
+ sourceRoleNormalization('administrator', 'Administrator', /^virtual\s+asistent$/iu),
83
+ sourceRoleNormalization('engineer', 'Engineer', /^(?:texnolog|technolog)\s+(?:injener|инженер)|^(?:injener|инженер)\s+(?:texnolog|technolog)/iu),
84
+ sourceRoleNormalization('electrician', 'Electrician', /^elektrik$/iu),
85
+ sourceRoleNormalization('construction_worker', 'Construction Worker', /^(?:yo['’ʻʼ‘`]?l|йул|йўл)\s+qurilish|^(?:yo['’ʻʼ‘`]?l|йул|йўл)\s+курилиш/iu),
86
+ sourceRoleNormalization('barista', 'Barista', /^(?:koffe|coffee)\s+ledy$/iu),
87
+ sourceRoleNormalization('chief_accountant', 'Chief Accountant', /^bosh\s+b(?:u(?:x|h)?|o)?galter$/iu),
88
+ sourceRoleNormalization('accountant', 'Accountant', /word[^\n]*excel[^\n]*hisob\s+kitob|^помо(?:ш|щ)ник\s+бухгалт/iu),
69
89
  sourceRoleNormalization('restaurant_cafe_worker', 'Restaurant / Cafe Worker', /^ищу\s+работу\s+(?:в\s+)?(?:кафе|ресторанах?|кафе\s+или\s+ресторанах)$/iu),
70
90
  sourceRoleNormalization('driver', 'Driver', /^(?:xaydovchilik|haydovchilik|shafyorlik|shofyorlik)/iu),
71
91
  sourceRoleNormalization('retail_worker', 'Retail Worker', /^do['’ʻʼ‘`]?kon$/iu),
@@ -79,7 +99,7 @@ export const SOURCE_ROLE_NORMALIZATION_RULES = Object.freeze([
79
99
  sourceRoleNormalization('hvac_technician', 'HVAC Technician', /^(?:konditsaner|kanditsaner|konditsioner)/iu),
80
100
  sourceRoleNormalization('mobile_content_creator', 'Mobile Content Creator', /^mobilografiya(?:\s+bo['’ʻʼ‘`]?yicha)?$/iu),
81
101
  sourceRoleNormalization('cctv_intercom_technician', 'CCTV / Intercom Technician', /kamera\s+(?:dama?fon|domofon)|domofon\s+xizmat/iu),
82
- sourceRoleNormalization('internal_control_specialist', 'Internal Control Specialist', /^ichki\s+nazoratchi$/iu),
102
+ sourceRoleNormalization('internal_control_specialist', 'Internal Control Specialist', /^(?:ichki\s+nazoratchi|внутренний\s+аудит)$/iu),
83
103
  sourceRoleNormalization('brand_ambassador', 'Brand Ambassador', /^(?:бренд\s+фейс|brand\s+face)$/iu),
84
104
  sourceRoleNormalization('insurance_specialist', 'Insurance Specialist', /^sug['’ʻʼ‘`]?urta$/iu),
85
105
  sourceRoleNormalization('bank_operations_specialist', 'Bank Operations Specialist', /^стаж[её]р\s+операционист|^операционист$/iu),
@@ -97,6 +117,15 @@ export function normalizeSourceRole(value) {
97
117
  return match ? Object.freeze({ canonical: match.canonical, label: match.label }) : null;
98
118
  }
99
119
 
120
+ /** Like normalizeSourceRole, but returns every display label a rule maps to
121
+ * (some raw values name two simultaneous roles, e.g. "Lawyer, Teacher"). */
122
+ export function normalizeSourceRoleKeys(value) {
123
+ const text = String(value || '').trim();
124
+ if (!text) return null;
125
+ const match = SOURCE_ROLE_NORMALIZATION_RULES.find((entry) => entry.re.test(text));
126
+ return match ? [...match.labels] : null;
127
+ }
128
+
100
129
  export const SOURCE_CANDIDATE_INTENT_ALIASES = Object.freeze([
101
130
  'работу ищу',
102
131
  'роботу шукаю',
@@ -18,7 +18,7 @@ export const PROPERTY_CONDITION_TERMS = Object.freeze([
18
18
  ]);
19
19
 
20
20
  export const LAYOUT_TERMS = Object.freeze([
21
- group('separateRooms', { ru: ['раздельные комнаты', 'изолированные комнаты'], en: ['separate rooms', 'isolated rooms'], uk: ['роздільні кімнати', 'ізольовані кімнати'], ro: ['camere separate'], uzLatn: ['alohida xonalar'], uzCyrl: ['алоҳида хоналар'], kk: ['бөлек бөлмелер'] }),
21
+ group('separateRooms', { ru: ['раздельные комнаты', 'изолированные комнаты'], en: ['separate rooms', 'isolated rooms'], uk: ['роздільні кімнати', 'ізольовані кімнати'], ro: ['camere separate'], uzLatn: ['alohida xonalar', 'aloxida xonalar'], uzCyrl: ['алоҳида хоналар', 'алохида хоналар'], kk: ['бөлек бөлмелер'] }),
22
22
  group('adjacentRooms', { ru: ['смежные комнаты', 'проходные комнаты', 'проходная комната'], en: ['adjacent rooms', 'walk-through room'], uk: ['суміжні кімнати', 'прохідна кімната'], ro: ['camere decomandate parțial', 'cameră de trecere'], uzLatn: ['o‘tish xonasi'], uzCyrl: ['ўтиш хонаси'], kk: ['өтпелі бөлме'] }),
23
23
  group('kitchenStudio', { ru: ['кухня-студия', 'кухня студия'], en: ['kitchen studio', 'open kitchen living room'], uk: ['кухня-студія'], ro: ['bucătărie open space'], uzLatn: ['oshxona studiya'], uzCyrl: ['ошхона студия'], kk: ['асүй-студия'] }),
24
24
  group('euroTwoRoom', { ru: ['евродвушка', 'евро двушка'], en: ['euro two-room', 'euro one bedroom'], uk: ['євродвушка'], ro: ['apartament euro două camere'], uzLatn: ['evro ikki xonali'], uzCyrl: ['евро икки хонали'], kk: ['еуро екі бөлмелі'] }),
@@ -29,7 +29,7 @@ export const LAYOUT_TERMS = Object.freeze([
29
29
  ]);
30
30
 
31
31
  export const BUILDING_TYPE_TERMS = Object.freeze([
32
- group('brick', { ru: ['кирпичный дом', 'кирпич'], en: ['brick building', 'brick house'], uk: ['цегляний будинок'], ro: ['clădire din cărămidă'], uzLatn: ['g‘ishtli uy', "g'ishtli uy"], uzCyrl: ['ғиштли уй'], kk: ['кірпіш үй'] }),
32
+ group('brick', { ru: ['кирпичный дом', 'кирпич'], en: ['brick building', 'brick house'], uk: ['цегляний будинок'], ro: ['clădire din cărămidă'], uzLatn: ['g‘ishtli uy', "g'ishtli uy", 'gishtli uy', 'g‘ishtli', "g'ishtli", 'gishtli'], uzCyrl: ['ғиштли уй', 'гиштли уй', 'ғиштли', 'гиштли'], kk: ['кірпіш үй'] }),
33
33
  group('panel', { ru: ['панельный дом', 'панелька'], en: ['panel building', 'prefab panel'], uk: ['панельний будинок'], ro: ['bloc din panouri'], uzLatn: ['panelli uy'], uzCyrl: ['панелли уй'], kk: ['панельді үй'] }),
34
34
  group('monolithic', { ru: ['монолитный дом', 'монолит'], en: ['monolithic building', 'monolith'], uk: ['монолітний будинок'], ro: ['clădire monolit'], uzLatn: ['monolit uy'], uzCyrl: ['монолит уй'], kk: ['монолитті үй'] }),
35
35
  group('monolithicBrick', { ru: ['монолитно-кирпичный', 'монолит кирпич'], en: ['monolithic brick'], uk: ['монолітно-цегляний'], ro: ['monolit-cărămidă'], uzLatn: ['monolit g‘isht'], uzCyrl: ['монолит ғишт'], kk: ['монолит кірпіш'] }),
@@ -116,7 +116,7 @@ export const FINANCING_TERMS = Object.freeze([
116
116
  ]);
117
117
 
118
118
  export const LOCATION_RELATIONS = Object.freeze([
119
- group('near', { ru: ['рядом с', 'возле', 'около'], en: ['near', 'next to', 'close to'], uk: ['поруч', 'біля'], ro: ['lângă', 'aproape de'], uzLatn: ['yaqinida', 'yonida', 'yaqin'], uzCyrl: ['яқинида', 'ёнида', 'яқин'], kk: ['жанында', 'маңында'] }),
119
+ group('near', { ru: ['рядом', 'рядом с', 'возле', 'около'], en: ['near', 'next to', 'close to'], uk: ['поруч', 'біля'], ro: ['lângă', 'aproape de'], uzLatn: ['yaqinida', 'yonida', 'yaqin'], uzCyrl: ['яқинида', 'ёнида', 'яқин'], kk: ['жанында', 'маңында'] }),
120
120
  group('opposite', { ru: ['напротив', 'через дорогу'], en: ['opposite', 'across the road'], uk: ['навпроти', 'через дорогу'], ro: ['vizavi', 'peste drum'], uzLatn: ['ro‘parasida'], uzCyrl: ['рўпарасида'], kk: ['қарсысында'] }),
121
121
  group('behind', { ru: ['за парком', 'за рынком', 'за базаром'], en: ['behind'], uk: ['за парком'], ro: ['în spatele'], uzLatn: ['orqasida'], uzCyrl: ['орқасида'], kk: ['артында'] }),
122
122
  group('intersection', { ru: ['на пересечении', 'на перекрёстке', 'перекрёсток'], en: ['at the intersection', 'crossroads'], uk: ['на перехресті'], ro: ['la intersecție'], uzLatn: ['chorrahada'], uzCyrl: ['чорраҳада'], kk: ['қиылыста'] }),
@@ -21,7 +21,7 @@ export function parseHousingFeatures(value) {
21
21
 
22
22
  const internet = bool(
23
23
  text,
24
- tokenRe(String.raw`интернет|wi[ -]?fi|we[ -]?fi|вай[ -]?фай|vayfay|роутер|router|інтернет`),
24
+ tokenRe(String.raw`интернет|wi[ -]?fi|we[ -]?fi|wf|вай[ -]?фай|vayfay|роутер|router|інтернет`),
25
25
  /(?:без\s+(?:интернет|wi[ -]?fi|вай[ -]?фай)|нет\s+(?:интернет\p{L}*|wi[ -]?fi)|(?:интернет|wi[ -]?fi)\s+нет|internet\s+(?:yo['’]?q|mavjud\s+emas)|интернет\s+йўқ)/iu,
26
26
  );
27
27
  const courtyard = bool(
@@ -22,7 +22,7 @@ const APPLIANCE_PATTERNS = Object.freeze([
22
22
  ['Microwave', /(?:микроволнов|microwave|mikrovolnov|mikravolnof)/iu],
23
23
  ['Vacuum cleaner', /(?:пылесос|vacuum\s+cleaner|pilesos|pedesos)/iu],
24
24
  ['Oven', /(?:духовк|oven|duxovka|duhofka|dxofka)/iu],
25
- ['Kitchen', /(?:кухн|kitchen|oshxona)/iu],
25
+ ['Kitchen', /(?:кухн|kitchen|oshxona|kuxn\p{L}*)/iu],
26
26
  ]);
27
27
 
28
28
  const FIRST_RENT_UZ_RE = /(?:hali\s+hech\s+kim\s+(?:yashamagan|turmagan)|ҳали\s+ҳеч\s+ким\s+(?:яшамаган|турмаган))/iu;
@@ -8,6 +8,7 @@ import {
8
8
  parseScaledAmount,
9
9
  } from './money-core.js';
10
10
  import { maskPhoneLikeSpans } from './contact.js';
11
+ import { DEPOSIT_TERMS, SELLER_TERMS } from './housing.js';
11
12
 
12
13
  const PRICE_KEYWORD = '(?:цена|ціна|нарх(?:и)?|narx(?:i)?|price|стоимост[ьи]|аренд(?:а|ная\\s+плата)?|rent)';
13
14
  // moneyCurrencyPattern() includes short codes (cad, ron, aed...) with no
@@ -19,11 +20,34 @@ const PRICE_KEYWORD = '(?:цена|ціна|нарх(?:и)?|narx(?:i)?|price|с
19
20
  const CURRENCY_ALT = `(?:${moneyCurrencyPattern()})`;
20
21
  const PRICE_CURRENCY_AFTER_NUMBER = `(?:${CURRENCY_ALT}(?![\\p{L}\\p{N}_]))`;
21
22
  const PRICE_CURRENCY_BEFORE_NUMBER = `(?:(?<![\\p{L}\\p{N}_])${CURRENCY_ALT})`;
23
+ const PAYMENT_AMOUNT_TERMS = Object.freeze([
24
+ DEPOSIT_TERMS.deposit,
25
+ SELLER_TERMS.commission,
26
+ ].filter(Boolean));
27
+ const PRICE_KEYWORD_RE = new RegExp(PRICE_KEYWORD, 'iu');
22
28
 
23
29
  function escapeRegex(value) {
24
30
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
25
31
  }
26
32
 
33
+ function isPaymentScopedAmount(text, start, end) {
34
+ const left = text
35
+ .slice(Math.max(0, start - 56), start)
36
+ .split(/[\r\n.;!?]/u)
37
+ .pop() || '';
38
+ const right = text
39
+ .slice(end, Math.min(text.length, end + 28))
40
+ .split(/[\r\n.;!?]/u)[0] || '';
41
+
42
+ // "deposit 500$" / "commission 100$" are payment details, not the
43
+ // listing price. For the reverse form ("500$ deposit"), only suppress the
44
+ // amount when there is no explicit price/rent label immediately to its left;
45
+ // this preserves text such as "rent 800$, deposit 500$".
46
+ if (findCanonical(left, PAYMENT_AMOUNT_TERMS, { partial: true })) return true;
47
+ return !PRICE_KEYWORD_RE.test(left)
48
+ && Boolean(findCanonical(right, PAYMENT_AMOUNT_TERMS, { partial: true }));
49
+ }
50
+
27
51
  const APPROXIMATE_RE = /около|примерно|~|≈/iu;
28
52
 
29
53
  export function parseHousingPrice(value, fallbackCurrency = '') {
@@ -78,11 +102,15 @@ export function parseHousingPrice(value, fallbackCurrency = '') {
78
102
  // 'u' is required for the \p{L}/\p{N} boundary escapes to work as
79
103
  // Unicode property classes — without it they silently match nothing,
80
104
  // which had made the boundary guard a no-op.
81
- const reNumSym = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*${PRICE_CURRENCY_AFTER_NUMBER}`, 'igu');
105
+ // Source ads sometimes insert a stray period before the currency symbol:
106
+ // "500.$". Treat that punctuation as a separator, not as part of the
107
+ // numeric amount.
108
+ const reNumSym = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*[.]?\\s*${PRICE_CURRENCY_AFTER_NUMBER}`, 'igu');
82
109
  const reSymNum = new RegExp(`${PRICE_CURRENCY_BEFORE_NUMBER}\\s*(${MONEY_NUMBER_PATTERN})`, 'igu');
83
110
  for (const regex of [reNumSym, reSymNum]) {
84
111
  let match;
85
112
  while ((match = regex.exec(text)) !== null) {
113
+ if (isPaymentScopedAmount(text, match.index ?? 0, regex.lastIndex)) continue;
86
114
  const amount = parseNumericAmount(match[1]);
87
115
  if (amount != null && amount >= 50 && amount <= 5_000_000_000 && (tagged == null || amount > tagged)) tagged = amount;
88
116
  }
@@ -118,9 +146,12 @@ export function parseHousingPrice(value, fallbackCurrency = '') {
118
146
  }
119
147
 
120
148
  if (price == null) {
121
- const matches = text.match(/\d{1,3}(?:[ \u00A0.,]\d{3})+|\d{4,}/g) || [];
149
+ const bareAmountRe = /\d{1,3}(?:[ \u00A0.,]\d{3})+|\d{4,}/g;
122
150
  let best = null;
123
- for (const raw of matches) {
151
+ for (const match of text.matchAll(bareAmountRe)) {
152
+ const raw = match[0];
153
+ const start = match.index ?? 0;
154
+ if (isPaymentScopedAmount(text, start, start + raw.length)) continue;
124
155
  const digits = raw.replace(/[\s.,]/g, '');
125
156
  if (digits[0] === '0') continue;
126
157
  const amount = parseNumericAmount(raw);
@@ -138,4 +169,4 @@ export function parseHousingPrice(value, fallbackCurrency = '') {
138
169
  }
139
170
 
140
171
  // Compatibility name for callers migrating from Flat Finder's local parser.
141
- export const parsePriceFromText = parseHousingPrice;
172
+ export const parsePriceFromText = parseHousingPrice;
@@ -23,7 +23,7 @@ export const HOUSING_LANDMARK_EXTENSIONS = Object.freeze([
23
23
  ru: ['детские сады', 'детские садики', 'детский садик', 'детский садики'],
24
24
  en: ['kindergartens', 'nurseries'],
25
25
  uzLatn: ["bog'chalar", 'bog‘chalar', 'bogʻchalar'],
26
- uzCyrl: ['боғчалар'],
26
+ uzCyrl: ['боғчалар', 'богча', 'богчалар'],
27
27
  }, { category: 'kindergarten' }),
28
28
  group('Bus stop', {
29
29
  ru: ['автобусной остановке', 'автобусная остановка рядом', 'остановка рядом'],
@@ -59,7 +59,7 @@ export const HOUSING_LANDMARK_EXTENSIONS = Object.freeze([
59
59
  ru: ['парки', 'рядом парки'],
60
60
  en: ['parks'],
61
61
  uzLatn: ["bog'lar", 'bog‘lar', 'bogʻlar'],
62
- uzCyrl: ['боғлар'],
62
+ uzCyrl: ['боғлар', 'сайлгоҳлар', 'сайлгох', 'сайлгохлар'],
63
63
  }, { category: 'park' }),
64
64
  group('Playground', {
65
65
  ru: ['детская площадка', 'детские площадки'],
@@ -95,7 +95,7 @@ export function parseHousingFloor(value) {
95
95
  let totalFloors = toNumber(explicit?.[2]);
96
96
 
97
97
  if (floor == null) {
98
- const beforeMarker = text.match(/(?:^|[^\d])(\d{1,3})\s*-?\s*(?:этаж(?:да)?|поверх|floor|etaj|qavat(?:da)?|қабат(?:та)?|кават(?:да)?|қават(?:да)?)(?=$|[^\p{L}\p{N}_])/iu);
98
+ const beforeMarker = text.match(/(?:^|[^\d])(\d{1,3})\s*-?\s*(?:(?:chi|чи)\s*)?(?:этаж(?:да)?|поверх|floor|etaj|qavat(?:i(?:da(?:gi)?)?|da)?|қабат(?:ы(?:нда(?:ғы)?)?|та)?|кават(?:и(?:да(?:ги)?)?|да)?|қават(?:и(?:да(?:ги)?)?|да)?)(?=$|[^\p{L}\p{N}_])/iu);
99
99
  floor = toNumber(beforeMarker?.[1]);
100
100
  }
101
101