@whiteslove/parsing-lexicon 0.10.0 → 0.10.1
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
package/src/geo.js
CHANGED
|
@@ -19,7 +19,7 @@ export const TASHKENT_DISTRICTS = Object.freeze([
|
|
|
19
19
|
entity('Shaykhantahur', { uzLatn: ['Shayxontohur', 'Shayxontohur tumani', 'Shaykhantahur'], uzCyrl: ['Шайхонтоҳур', 'Шайхонтоҳур тумани', 'Шайхонтохур'], ru: ['Шайхантахур', 'Шайхантахурский район'], en: ['Shaykhantahur', 'Shayxontohur'] }),
|
|
20
20
|
entity('Yunusabad', { uzLatn: ['Yunusobod', 'Yunusobod tumani', 'Yunusabad'], uzCyrl: ['Юнусобод', 'Юнусобод тумани'], ru: ['Юнусабад', 'Юнусабадский район'], en: ['Yunusabad', 'Yunusobod'] }),
|
|
21
21
|
entity('Yakkasaray', { uzLatn: ['Yakkasaroy', 'Yakkasaroy tumani', 'Yakkasaray'], uzCyrl: ['Яккасарой', 'Яккасарой тумани'], ru: ['Яккасарай', 'Яккасарайский район'], en: ['Yakkasaray', 'Yakkasaroy'] }),
|
|
22
|
-
entity('Yangihayot', { uzLatn: ['Yangihayot', 'Yangihayot tumani'], uzCyrl: ['Янгиҳаёт', 'Янгиҳаёт тумани', 'Янгихаёт'], ru: ['Янгихаётский район', 'Янгихаёт'], en: ['Yangihayot'] }),
|
|
22
|
+
entity('Yangihayot', { uzLatn: ['Yangihayot', 'Yangihayot tumani'], uzCyrl: ['Янгиҳаёт', 'Янгиҳаёт тумани', 'Янгихаёт'], ru: ['Янгихаётский район', 'Янгихаёт', 'Янгихаетский район', 'Янгихаетский', 'Янгихает'], en: ['Yangihayot'] }),
|
|
23
23
|
entity('Yashnobod', { uzLatn: ['Yashnobod', 'Yashnobod tumani', 'Yashnabod', 'Yashnabod tumani', 'Yashnobot', 'Yashnobot tumani'], uzCyrl: ['Яшнобод', 'Яшнобод тумани'], ru: ['Яшнабад', 'Яшнабадский район'], en: ['Yashnobod', 'Yashnabad'] }),
|
|
24
24
|
]);
|
|
25
25
|
|
|
@@ -32,7 +32,7 @@ const APPLIANCE_PATTERNS = Object.freeze([
|
|
|
32
32
|
|
|
33
33
|
const FIRST_RENT_UZ_RE = /(?:hali\s+hech\s+kim\s+(?:yashamagan|turmagan)|ҳали\s+ҳеч\s+ким\s+(?:яшамаган|турмаган))/iu;
|
|
34
34
|
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;
|
|
35
|
-
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+үшін|(?:oila|oyla)(?:ga|lar|li)?\s+yoki\s+\d{1,2}\s+ta\s+bola(?:lar)?(?:ga)?\s+(?:ijara(?:ga)?\s+)?(?:beril|topshiril))/iu;
|
|
35
|
+
const STUDENT_RE = /(?:studentlar\s+uchun|talabalar\s+uchun|(?<![\p{L}\p{N}_])(?:student(?:ka)?|talaba)(?:lar)?(?:ga)?\s+(?:qiz|yigit|o['’ʻʼ‘`]?g['’ʻʼ‘`]?il\s+bola|bola)(?:lar)?(?:ga)?(?![\p{L}\p{N}_])|(?<![\p{L}\p{N}_])(?:studentlar|talabalar)ga(?![\p{L}\p{N}_])|студент(?:ам|ы|ок|ов)?\s+(?:можно|для)|для\s+студент|students?\s+(?:only|welcome)|for\s+students|pentru\s+studen[țt]i|studen[țt]i(?:lor)?|студенттерге|студенттер\s+үшін|(?:oila|oyla)(?:ga|lar|li)?\s+yoki\s+\d{1,2}\s+ta\s+bola(?:lar)?(?:ga)?\s+(?:ijara(?:ga)?\s+)?(?:beril|topshiril))/iu;
|
|
36
36
|
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;
|
|
37
37
|
const BROKER_RE = /(?:makler|vositachi|макл(?:ер[а-яё]*)?|ри[еэ]лтор[а-яё]*|агентств[а-яё]*|комисси[а-яё]*|broker|realtor|commission|comision(?:ul)?|agen[țt]ie|delda[lл]\p{L}*|делдал\p{L}*)/iu;
|
|
38
38
|
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;
|
|
@@ -45,6 +45,10 @@ const FAMILY_EXCLUSION_RE = /(?:oila|oyla|семь\p{L}*|family|famil(?:ie|ia)|c
|
|
|
45
45
|
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;
|
|
46
46
|
const AIR_CONDITIONER_RE = /(?:кондицион|air\s*con|konditsioner|kandit(?:s|c)?aner|kanditsaner|кандитсанер)/iu;
|
|
47
47
|
const PER_PERSON_PRICE_RE = /(?:kishi\s+boshiga|киши\s+бошига)\s*(\d{1,3}(?:[\s.,]\d{3})*|\d+(?:[.,]\d+)?)\s*(ming|минг|million|mln|млн)?(?:dan|дан)?/iu;
|
|
48
|
+
// Shared-flat posts price each tenant with the ablative "-dan" ("800 000
|
|
49
|
+
// so'mdan" = "800 000 so'm from each") once they ask for several people.
|
|
50
|
+
const PER_PERSON_SUFFIX_PRICE_RE = /(\d{1,3}(?:[ ]\d{3})+|\d+)\s*(ming|минг|million|mln|млн)?\s*(?:so['’ʻʼ‘`]?m|сўм|сум)(?:dan|дан)(?![\p{L}\p{N}_])/iu;
|
|
51
|
+
const SEVERAL_OCCUPANTS_RE = /(?<![\p{L}\p{N}_])([2-9])\s*ta\s+(?:qiz|yigit|bola|kishi|odam|talaba|student)\p{L}*\s+(?:kerak|kere|olinadi)/iu;
|
|
48
52
|
const WALK_MINUTES_RE = /(?:yayov|piyoda|пешком)\s*(\d{1,2})\s*(?:daqiqa|min(?:ute)?s?|минут)/iu;
|
|
49
53
|
const TRANSIT_ROUTES_RE = /(?:aftobuslar|avtobuslar|автобуслар|автобусы)[^\r\n\d]{0,24}((?:\d{1,4}[\s,;/]*){1,10})/iu;
|
|
50
54
|
const NEARBY_RELATION_TAIL_RE = /(?<!\p{L})(?:рядом\s+(?:с|со)|недалеко\s+от|возле|около|напротив|навпроти|ориентир\s*[:—–-]?|ор[-–—]?р\.?\s*[:—–-]?|near(?:by)?|close\s+to|next\s+to|opposite|behind|in\s+front\s+of|yaqin(?:ida)?|lângă|aproape\s+de|în\s+apropiere\s+de|vizavi\s+de|în\s+spatele|în\s+fața)(?!\p{L})[^.!?\r\n;]*/giu;
|
|
@@ -194,7 +198,9 @@ export function parseHousingAudience(value) {
|
|
|
194
198
|
const students = STUDENT_RE.test(text);
|
|
195
199
|
if (family && students) return deepFreeze({ primary: 'family', alternatives: ['family', 'students'] });
|
|
196
200
|
if (family && women) return deepFreeze({ primary: 'family', alternatives: ['family', 'women'] });
|
|
197
|
-
|
|
201
|
+
// "Student qizlarga" narrows students to girls: the gender is the binding
|
|
202
|
+
// restriction, and studentTarget still reports the student part.
|
|
203
|
+
if (students && !women && !men) return deepFreeze({ primary: 'students', alternatives: ['students'] });
|
|
198
204
|
if (family) return deepFreeze({ primary: 'family', alternatives: ['family'] });
|
|
199
205
|
if (women) return deepFreeze({ primary: 'women', alternatives: ['women'] });
|
|
200
206
|
if (men) return deepFreeze({ primary: 'men', alternatives: ['men'] });
|
|
@@ -222,14 +228,15 @@ export function parseHousingCommission(value) {
|
|
|
222
228
|
|
|
223
229
|
export function parseHousingPerPersonPrice(value, { country = '' } = {}) {
|
|
224
230
|
const text = normalizeUnicode(value ?? '');
|
|
225
|
-
const match = text.match(PER_PERSON_PRICE_RE)
|
|
231
|
+
const match = text.match(PER_PERSON_PRICE_RE)
|
|
232
|
+
|| (SEVERAL_OCCUPANTS_RE.test(text) ? text.match(PER_PERSON_SUFFIX_PRICE_RE) : null);
|
|
226
233
|
if (!match) return null;
|
|
227
234
|
let amount = Number(String(match[1]).replace(/\s+/g, '').replace(',', '.'));
|
|
228
235
|
if (!Number.isFinite(amount) || amount <= 0) return null;
|
|
229
236
|
const scale = String(match[2] || '').toLowerCase();
|
|
230
237
|
if (scale === 'ming' || scale === 'минг') amount *= 1000;
|
|
231
238
|
else if (scale === 'million' || scale === 'mln' || scale === 'млн') amount *= 1_000_000;
|
|
232
|
-
const currency = String(country || '').toUpperCase() === 'UZ' || /(?:ming
|
|
239
|
+
const currency = String(country || '').toUpperCase() === 'UZ' || /(?:ming|минг|so['’ʻʼ‘`]?m|сўм|сум)/iu.test(match[0]) ? 'UZS' : null;
|
|
233
240
|
const approximate = /около|примерно|~|≈/iu.test(text);
|
|
234
241
|
return deepFreeze({ amount, currency, approximate, scope: 'person' });
|
|
235
242
|
}
|
package/src/housing-money.js
CHANGED
|
@@ -602,7 +602,10 @@ export function parseHousingPrice(value, context = '') {
|
|
|
602
602
|
const hasExplicitUzbekThousandScale = country === 'UZ' && /\b(?:ming|минг)\b/iu.test(priceText);
|
|
603
603
|
if (!explicit && !hasExplicitUzbekThousandScale && fallbackCurrency === 'UZS' && price != null) {
|
|
604
604
|
const dailyUzbek = /(?:kunlik|sutkaga|kecha[- ]?kunduz|посуточн|суточн)/i.test(priceText);
|
|
605
|
-
|
|
605
|
+
// A monthly rent of 100,000+ is only plausible in so'm; the USD reading
|
|
606
|
+
// turned "800 000" per person into an 800,000 USD rent.
|
|
607
|
+
const monthlyRent = dealType === 'longRent' && price >= 100_000;
|
|
608
|
+
currency = price >= 1_000_000 || monthlyRent || (dailyUzbek && price >= 10_000) ? 'UZS' : 'USD';
|
|
606
609
|
}
|
|
607
610
|
|
|
608
611
|
return Object.freeze({ amount: price, currency, approximate: price != null && APPROXIMATE_RE.test(priceText) });
|
|
@@ -87,7 +87,9 @@ export function parseHousingRoomCount(value) {
|
|
|
87
87
|
if (total >= 1 && total <= 20) return total;
|
|
88
88
|
}
|
|
89
89
|
for (const [re, rooms] of NUMBER_WORDS) if (re.test(text)) return rooms;
|
|
90
|
-
|
|
90
|
+
// "5-х комн.кв" / "3-ех комнатная": a Russian case ending may sit between
|
|
91
|
+
// the number and the room word, which may itself be abbreviated.
|
|
92
|
+
const numeric = text.match(/(?:^|[^\p{L}\p{N}])(\d{1,2})\s*(?:-\s*(?:х|ти|и|ух|[её]х)\s*)?(?:ta\s*)?(?:(?:-\s*)?комн(?:ат)?\p{L}*|(?:-\s*)?к(?:\.|\b)|(?:-\s*)?xona(?:li|si|lari)?|(?:-\s*)?хона(?:лик|ли|си|лари)?|бөлмелі|rooms?)(?=$|[^\p{L}\p{N}])/iu);
|
|
91
93
|
if (numeric) {
|
|
92
94
|
const rooms = toNumber(numeric[1]);
|
|
93
95
|
if (rooms != null && rooms >= 1 && rooms <= 20) return rooms;
|
|
@@ -127,7 +129,8 @@ export function parseHousingFloor(value) {
|
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
// "3-й этаж" / "5-м этаже" carry a Russian ordinal ending like Uzbek "-chi".
|
|
133
|
+
const beforeMarker = text.match(/(?:^|[^\d])(\d{1,3})\s*-?\s*(?:(?:chi|чи|й|ий|ый|ой|м|ом|ем)\s*)?(?:этаж(?:да)?|поверх|floor|etaj|qavat(?:i(?:da(?:gi)?)?|da)?|қабат(?:ы(?:нда(?:ғы)?)?|та)?|кават(?:и(?:да(?:ги)?)?|да)?|қават(?:и(?:да(?:ги)?)?|да)?)(?=$|[^\p{L}\p{N}_])/iu);
|
|
131
134
|
let floor = toNumber(beforeMarker?.[1]);
|
|
132
135
|
let totalFloors = null;
|
|
133
136
|
|
|
@@ -140,7 +143,7 @@ export function parseHousingFloor(value) {
|
|
|
140
143
|
if (floor == null && FIRST_FLOOR_WORD_RE.test(text)) floor = 1;
|
|
141
144
|
|
|
142
145
|
if (totalFloors == null) {
|
|
143
|
-
const total = text.match(/(?:дом\s*)?(\d{1,3})\s*(?:[- ]?этаж(?:н\p{L}*|лик)|поверхов\p{L}*|storey|story|floors?\s+total|qavatli|қабатты|каватли|қаватли)/iu);
|
|
146
|
+
const total = text.match(/(?:дом\s*)?(\d{1,3})\s*(?:-\s*(?:ти|х|и|ми)\s*)?(?:[- ]?этаж(?:н\p{L}*|лик)|поверхов\p{L}*|storey|story|floors?\s+total|qavatli|қабатты|каватли|қаватли)/iu);
|
|
144
147
|
totalFloors = toNumber(total?.[1]);
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -167,7 +170,7 @@ const AREA_UNIT_RE = String.raw`(?:м²|м2|m²|m2|sqm|sq\.?\s*m|mp|кв\.?\s*м
|
|
|
167
170
|
const AREA_UNIT_AFTER_LABEL_RE = String.raw`(?:${AREA_UNIT_RE}|[mм])`;
|
|
168
171
|
|
|
169
172
|
function areaAfterLabel(text, labelRe) {
|
|
170
|
-
const re = new RegExp(`${labelRe.source}\\s*[:=-]?\\s*(\\d{1,4}(?:[.,]\\d{1,2})?)\\s*${AREA_UNIT_AFTER_LABEL_RE}(?=$|[^\\p{L}\\p{N}_])`, 'iu');
|
|
173
|
+
const re = new RegExp(`${labelRe.source}\\s*[:=-]?\\s*(\\d{1,4}(?:[.,]\\d{1,2})?)\\s*-?\\s*${AREA_UNIT_AFTER_LABEL_RE}(?=$|[^\\p{L}\\p{N}_])`, 'iu');
|
|
171
174
|
return toNumber(text.match(re)?.[1]);
|
|
172
175
|
}
|
|
173
176
|
|
package/src/money-lexicon.js
CHANGED
|
@@ -5,7 +5,7 @@ const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, alias
|
|
|
5
5
|
export const CURRENCY_TERMS = Object.freeze([
|
|
6
6
|
group('USD', { ru: ['$', 'usd', 'доллар', 'доллара', 'долларов', 'бакс', 'у.е.', 'у е'], en: ['$', 'usd', 'dollar', 'dollars', 'us dollar'], uk: ['$', 'usd', 'долар', 'долари', 'доларів'], ro: ['$', 'usd', 'dolar', 'dolari'], uzLatn: ['$', 'usd', 'dollar'], uzCyrl: ['$', 'доллар'], kk: ['$', 'usd', 'доллар'] }),
|
|
7
7
|
group('EUR', { ru: ['€', 'eur', 'евро'], en: ['€', 'eur', 'euro', 'euros'], uk: ['€', 'eur', 'євро'], ro: ['€', 'eur', 'euro'], uzLatn: ['€', 'eur', 'yevro'], uzCyrl: ['€', 'евро'], kk: ['€', 'eur', 'еуро'] }),
|
|
8
|
-
group('UZS', { ru: ['сум', 'сумов', 'узс'], en: ['uzs', 'uzbek som'], uk: ['uzs', 'сум'], ro: ['uzs'], uzLatn: ["so'm", 'so‘m', 'soʻm', 'sum', 'uzs'], uzCyrl: ['сўм', 'сум'], kk: ['uzs'] }),
|
|
8
|
+
group('UZS', { ru: ['сум', 'сумов', 'узс'], en: ['uzs', 'uzbek som'], uk: ['uzs', 'сум'], ro: ['uzs'], uzLatn: ["so'm", 'so‘m', 'soʻm', 'sum', 'uzs', "so'mdan", 'so‘mdan', 'soʻmdan', 'somdan', 'sumdan', "so'mgacha", 'so‘mgacha', 'soʻmgacha'], uzCyrl: ['сўм', 'сум', 'сўмдан', 'сумдан', 'сўмгача'], kk: ['uzs'] }),
|
|
9
9
|
group('KZT', { ru: ['₸', 'тенге', 'тг', 'kzt'], en: ['₸', 'kzt', 'tenge'], uk: ['₸', 'тенге', 'kzt'], ro: ['₸', 'kzt', 'tenge'], uzLatn: ['₸', 'kzt', 'tenge'], uzCyrl: ['₸', 'kzt', 'тенге'], kk: ['₸', 'теңге', 'тенге', 'тг', 'kzt'] }),
|
|
10
10
|
group('UAH', { ru: ['грн', 'гривна', 'гривны', 'гривен', '₴', 'uah'], en: ['uah', 'hryvnia', 'hryvnias', '₴'], uk: ['грн', 'гривня', 'гривні', 'гривень', '₴', 'uah'], ro: ['uah', 'hrivne', 'grivne'], uzLatn: ['uah'], uzCyrl: ['uah'], kk: ['uah'] }),
|
|
11
11
|
group('RUB', { ru: ['₽', 'руб', 'руб.', 'рубль', 'рубля', 'рублей', 'rub', 'rur'], en: ['₽', 'rub', 'rur', 'ruble', 'rubles'], uk: ['₽', 'руб', 'рублів', 'rub'], ro: ['₽', 'rub', 'ruble'], uzLatn: ['₽', 'rub', 'rubl'], uzCyrl: ['₽', 'рубль'], kk: ['₽', 'rub', 'рубль'] }),
|
|
@@ -167,7 +167,7 @@ export const TASHKENT_NUMBERED_AREA_ALIASES = Object.freeze({
|
|
|
167
167
|
Kuylyuk: Object.freeze(['куйлюк', 'куйлик', 'kuylyuk', 'kuyliq', 'qoyliq', 'qo yliq']),
|
|
168
168
|
Sergeli: Object.freeze(['сергели', 'sergeli', 'sergile', 'sergele']),
|
|
169
169
|
Yunusabad: Object.freeze(['юнусабад', 'yunusabad', 'yunusobod']),
|
|
170
|
-
Yangihayot: Object.freeze(['янгихаёт', 'янгихаят', 'yangihayot']),
|
|
170
|
+
Yangihayot: Object.freeze(['янгихаёт', 'янгихает', 'янгихаят', 'yangihayot']),
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
const REVERSE_NUMBERED_ALIASES = Object.freeze({
|
|
@@ -241,15 +241,24 @@ export function hasExplicitTashkentDistrict(value, canonical) {
|
|
|
241
241
|
export function matchTashkentHousingDistrict(value) {
|
|
242
242
|
const text = String(value ?? '');
|
|
243
243
|
if (!text) return null;
|
|
244
|
+
let explicit = null;
|
|
245
|
+
let implicit = null;
|
|
244
246
|
for (const district of TASHKENT_HOUSING_DISTRICTS) {
|
|
245
247
|
const match = text.match(district.re);
|
|
246
248
|
if (!match) continue;
|
|
247
|
-
|
|
249
|
+
// The earliest explicitly marked district wins. In "Янгихаетский район
|
|
250
|
+
// Сергели-1" the "район" also precedes Сергели, but it belongs to the
|
|
251
|
+
// adjective before it; Sergeli-1 is only the quarter name.
|
|
252
|
+
if (hasExplicitDistrictContext(text, match)) {
|
|
253
|
+
if (!explicit || match.index < explicit.index) explicit = { district, index: match.index };
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (implicit) continue;
|
|
248
257
|
if (hasExplicitMetroContext(text, match) || hasExplicitAreaContext(text, match) || hasExplicitMahallaContext(text, match)) continue;
|
|
249
258
|
if (matchTashkentNumberedArea(text, district.name)) continue;
|
|
250
|
-
|
|
259
|
+
implicit = district;
|
|
251
260
|
}
|
|
252
|
-
return
|
|
261
|
+
return explicit?.district || implicit;
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
/**
|