@whiteslove/parsing-lexicon 0.3.2 → 0.3.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.
- package/package.json +1 -1
- package/src/hiring-source-aliases.d.ts +2 -0
- package/src/hiring-source-aliases.js +32 -3
- package/src/housing-context.js +3 -3
- package/src/housing-features.js +1 -1
- package/src/housing-listing-enrichment.js +1 -1
- package/src/housing-money.js +35 -4
- package/src/housing-poi-extensions.js +2 -2
- package/src/housing-structured.js +1 -1
package/package.json
CHANGED
|
@@ -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({
|
|
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,24 @@ 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)?|
|
|
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('it_specialist', 'IT Specialist', /^(?:kompyuter\s+(?:sohasida|xizmatlari\s+bo['’ʻʼ‘`]?yicha\s+ish\s+kerak)|it\s+kompyuter)$/iu),
|
|
80
|
+
sourceRoleNormalization('restaurant_manager', 'Restaurant Manager', /^(?:restoran|restaurant)[^\n]*(?:boshqaruv|manager|menejer)/iu),
|
|
81
|
+
sourceRoleNormalization('administrator', 'Administrator', /^virtual\s+asistent$/iu),
|
|
82
|
+
sourceRoleNormalization('engineer', 'Engineer', /^(?:texnolog|technolog)\s+(?:injener|инженер)|^(?:injener|инженер)\s+(?:texnolog|technolog)/iu),
|
|
83
|
+
sourceRoleNormalization('electrician', 'Electrician', /^elektrik$/iu),
|
|
84
|
+
sourceRoleNormalization('construction_worker', 'Construction Worker', /^(?:yo['’ʻʼ‘`]?l|йул|йўл)\s+qurilish|^(?:yo['’ʻʼ‘`]?l|йул|йўл)\s+курилиш/iu),
|
|
85
|
+
sourceRoleNormalization('barista', 'Barista', /^(?:koffe|coffee)\s+ledy$/iu),
|
|
86
|
+
sourceRoleNormalization('chief_accountant', 'Chief Accountant', /^bosh\s+b(?:u(?:x|h)?|o)?galter$/iu),
|
|
87
|
+
sourceRoleNormalization('accountant', 'Accountant', /word[^\n]*excel[^\n]*hisob\s+kitob|^помо(?:ш|щ)ник\s+бухгалт/iu),
|
|
69
88
|
sourceRoleNormalization('restaurant_cafe_worker', 'Restaurant / Cafe Worker', /^ищу\s+работу\s+(?:в\s+)?(?:кафе|ресторанах?|кафе\s+или\s+ресторанах)$/iu),
|
|
70
89
|
sourceRoleNormalization('driver', 'Driver', /^(?:xaydovchilik|haydovchilik|shafyorlik|shofyorlik)/iu),
|
|
71
90
|
sourceRoleNormalization('retail_worker', 'Retail Worker', /^do['’ʻʼ‘`]?kon$/iu),
|
|
@@ -79,7 +98,7 @@ export const SOURCE_ROLE_NORMALIZATION_RULES = Object.freeze([
|
|
|
79
98
|
sourceRoleNormalization('hvac_technician', 'HVAC Technician', /^(?:konditsaner|kanditsaner|konditsioner)/iu),
|
|
80
99
|
sourceRoleNormalization('mobile_content_creator', 'Mobile Content Creator', /^mobilografiya(?:\s+bo['’ʻʼ‘`]?yicha)?$/iu),
|
|
81
100
|
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),
|
|
101
|
+
sourceRoleNormalization('internal_control_specialist', 'Internal Control Specialist', /^(?:ichki\s+nazoratchi|внутренний\s+аудит)$/iu),
|
|
83
102
|
sourceRoleNormalization('brand_ambassador', 'Brand Ambassador', /^(?:бренд\s+фейс|brand\s+face)$/iu),
|
|
84
103
|
sourceRoleNormalization('insurance_specialist', 'Insurance Specialist', /^sug['’ʻʼ‘`]?urta$/iu),
|
|
85
104
|
sourceRoleNormalization('bank_operations_specialist', 'Bank Operations Specialist', /^стаж[её]р\s+операционист|^операционист$/iu),
|
|
@@ -88,6 +107,7 @@ export const SOURCE_ROLE_NORMALIZATION_RULES = Object.freeze([
|
|
|
88
107
|
sourceRoleNormalization('healthcare_specialist', 'Healthcare Specialist', /^mededsina$|^meditsina$|^медицина$/iu),
|
|
89
108
|
sourceRoleNormalization('tourism_hospitality_specialist', 'Tourism / Hospitality Specialist', /^mehmonxona[^\n]*turfirma|^turfirma[^\n]*mehmonxona/iu),
|
|
90
109
|
sourceRoleNormalization('confectioner', 'Confectioner', /qandolat|qandolatchi/iu),
|
|
110
|
+
sourceRoleNormalization('factory_worker', 'Factory Worker', /^(?:jizzax\s+)?kia\s+zavodidan\s+ish\s+kerak$|^zavod\s+ishlari(?:\s+.*)?$/iu),
|
|
91
111
|
]);
|
|
92
112
|
|
|
93
113
|
export function normalizeSourceRole(value) {
|
|
@@ -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
|
'роботу шукаю',
|
package/src/housing-context.js
CHANGED
|
@@ -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: ['қиылыста'] }),
|
package/src/housing-features.js
CHANGED
|
@@ -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;
|
package/src/housing-money.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
149
|
+
const bareAmountRe = /\d{1,3}(?:[ \u00A0.,]\d{3})+|\d{4,}/g;
|
|
122
150
|
let best = null;
|
|
123
|
-
for (const
|
|
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)?|қабат(
|
|
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
|
|