@whiteslove/parsing-lexicon 0.2.2 → 0.2.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 +2 -1
- package/src/housing-text.d.ts +2 -0
- package/src/housing-text.js +75 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whiteslove/parsing-lexicon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Shared deterministic multilingual parsing lexicon for Whiteslove housing and hiring services",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"./housing-context": { "types": "./src/housing-context.d.ts", "import": "./src/housing-context.js" },
|
|
42
42
|
"./housing-address": { "types": "./src/housing-address.d.ts", "import": "./src/housing-address.js" },
|
|
43
43
|
"./housing-structured": { "types": "./src/housing-structured.d.ts", "import": "./src/housing-structured.js" },
|
|
44
|
+
"./housing-text": { "types": "./src/housing-text.d.ts", "import": "./src/housing-text.js" },
|
|
44
45
|
"./housing-source-aliases": { "types": "./src/housing-source-aliases.d.ts", "import": "./src/housing-source-aliases.js" },
|
|
45
46
|
"./hiring": "./src/hiring.js",
|
|
46
47
|
"./hiring-languages": "./src/hiring-languages.js",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const ok10 = (n) => (n >= 1 && n <= 10 ? n : null);
|
|
2
|
+
|
|
3
|
+
const WORD_ROOMS = Object.freeze({
|
|
4
|
+
одно: 1, одн: 1, двух: 2, двох: 2, трех: 3, трёх: 3, трих: 3, трьох: 3,
|
|
5
|
+
четырех: 4, четырёх: 4, чотирьох: 4, чотирох: 4, пяти: 5,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export function parseHousingRoomsFromText(value) {
|
|
9
|
+
const text = String(value || '');
|
|
10
|
+
if (!text) return null;
|
|
11
|
+
const basement = text.match(/(?:^|\n)[^\d\r\n]{0,8}([1-9])\s*\/\s*0\s*\/\s*-1\s*(?:этаж|эт\.?)?[^\r\n]*(?:подвал|цоколь)/im);
|
|
12
|
+
if (basement) return ok10(Number(basement[1]));
|
|
13
|
+
const compact = text.match(/(?:^|\n)[^\d\r\n]{0,8}([1-9])\s*([¹²³⁴⁵⁶⁷⁸⁹])?\s*\/\s*([0-9]{1,2})\s*\/\s*([0-9]{1,2})[.,;:]?(?=\s|$)/m);
|
|
14
|
+
if (compact) {
|
|
15
|
+
const superscript = '¹²³⁴⁵⁶⁷⁸⁹'.indexOf(compact[2]) + 1;
|
|
16
|
+
if (superscript) return ok10(superscript);
|
|
17
|
+
const first = Number(compact[1]);
|
|
18
|
+
const floor = Number(compact[3]);
|
|
19
|
+
const last = Number(compact[4]);
|
|
20
|
+
if (first === floor && last > first && last <= 5) return ok10(last);
|
|
21
|
+
return ok10(first);
|
|
22
|
+
}
|
|
23
|
+
const after = text.match(/(?:количество\s+комнат\w*|комнат(?:ы|а)?(?![а-яё])|кімнат(?:и|а)?(?![а-яіїґ])|xonalar\s*soni|xona\s*soni|хоналар\s*сони|хона\s*сони|number\s+of\s+rooms)[^\S\r\n]*[:\-–—]?[^\S\r\n]*(\d+)/i);
|
|
24
|
+
if (after) return ok10(Number(after[1]));
|
|
25
|
+
const before = text.match(/(\d+)\s*[-хx]?\s*(?:camer|комнатн|комн|ком\.|кімнатн|кімн|кім\.|room|bedroom|xonali|xona|хонали|хона|бөлмел|бөлме)|(\d+)\s*-\s*к(?:омн|\.?\s*кв)/i);
|
|
26
|
+
if (before) return ok10(Number(before[1] ?? before[2]));
|
|
27
|
+
const word = text.toLowerCase().match(/(одно|одн|двух|двох|тр[еёи]х|трьох|четыр[её]х|чотир(?:ьох|ох)|пяти)\s*-?\s*(?:комнат|кімнат)/);
|
|
28
|
+
return word ? ok10(WORD_ROOMS[word[1]] ?? null) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const RC_STOP_WORD_RE = /^(?:жк|жм|продаётся|продается|продам|продажа|продаю|срочно|сдаётся|сдается|сдаю|сдам|аренда|арендую|новая|новый|новое|новые|новостройка|вторичка|квартира|квартиры|квартиру|комната|комнаты|комнат|комнатная|комнатной|этаж|этажа|этаж[её]м|ремонт|ремонтом|евроремонт|мебель|мебелью|мебелированная|дом|дома|тел|телефон|цена|торг|собственник|риелтор|посредник|uy|kvartira|xonali|sotiladi|ijaraga|yangi|arzon|tel|narx|new|newbuild|apartment|apartments|flat|for|sale|rent|rooms?|floor|предлагается|предлагаются|предлагаем|продаже|продаж|куплю|обмен|просторные|просторная|две|три|четыре|пять|район|районе|районы|туман|тумани|tumani|district|массив|массиве|massiv|метро|metro|рядом|около|возле|яндекс|город|шахар|shahar)$/iu;
|
|
32
|
+
const RC_ATTRIBUTE_TOKEN_RE = /^\d|^[\d/]+$|^[a-zа-яё]?\d+(?:[а-яёa-z]{1,4})?$/iu;
|
|
33
|
+
const RC_LATIN_NOISE_RE = /^(?:vip|lux|luxe|elite|premium|euro|evro|new|top|super|best|hot|urgent|srochno|arenda|ijara|sotiladi|sale|rent|for|home|house|flat|apartment|apartments|kvartira|tashkent|toshkent|wifi|wi|fi|tv|ac|internet|telegram|yandex|google|instagram|whatsapp|iphone|samsung|lg|bosch|artel|km|km2|m2|sqm|usd|uzs|eur)$/iu;
|
|
34
|
+
const RC_LATIN_WORD_RE = /^[A-Za-z][A-Za-z'’\-]{1,}$/u;
|
|
35
|
+
const RC_DISTRICT_MARKER_RE = /^(?:район|районе|районы|туман|тумани|tumani|district)$/iu;
|
|
36
|
+
const RC_MARKER_RE = /(?:жк|жм|ж\/к|residential complex|ansamblu(?: rezidential)?|turar[- ]?joy majmuasi)\s*/i;
|
|
37
|
+
|
|
38
|
+
function leadingLatinName(text) {
|
|
39
|
+
const cyrillic = (String(text).match(/[а-яёіїґ]/giu) || []).length;
|
|
40
|
+
const latin = (String(text).match(/[a-z]/giu) || []).length;
|
|
41
|
+
if (cyrillic <= latin) return null;
|
|
42
|
+
const tokens = String(text).trim().split(/[\s|,;:!]+/).filter(Boolean).slice(0, 12);
|
|
43
|
+
const runs = [];
|
|
44
|
+
let current = [];
|
|
45
|
+
for (const token of tokens) {
|
|
46
|
+
const clean = token.replace(/^[«»"'„“]+|[«»"'„“!|,]+$/g, '');
|
|
47
|
+
const bare = clean.replace(/[-'’]/g, '');
|
|
48
|
+
const usable = RC_LATIN_WORD_RE.test(clean) && !RC_LATIN_NOISE_RE.test(clean) && !RC_LATIN_NOISE_RE.test(bare) && !RC_STOP_WORD_RE.test(clean);
|
|
49
|
+
if (usable && current.length < 3) current.push(clean);
|
|
50
|
+
else { if (current.length) runs.push(current); current = usable ? [clean] : []; }
|
|
51
|
+
}
|
|
52
|
+
if (current.length) runs.push(current);
|
|
53
|
+
const named = runs.find((run) => run.some((word) => word.length >= 3) && run.length < tokens.length);
|
|
54
|
+
return named ? named.join(' ') : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseHousingResidentialComplex(value) {
|
|
58
|
+
const text = String(value || '');
|
|
59
|
+
if (!text) return null;
|
|
60
|
+
const marker = text.match(RC_MARKER_RE);
|
|
61
|
+
if (!marker) return leadingLatinName(text);
|
|
62
|
+
const rest = text.slice(marker.index + marker[0].length).replace(/^[\s:;;—–\-·•]+/, '');
|
|
63
|
+
const quoted = rest.match(/^["'«»„“]\s*([^"'«»„“\n]{2,60})["'«»„“]?/);
|
|
64
|
+
const candidate = quoted ? quoted[1] : (rest.match(/^([^"'«»„“\n,.;:()!|]{2,60})/) || [, ''])[1];
|
|
65
|
+
const parsed = String(candidate).trim().replace(/([\p{Ll}\d])(\p{Lu})/gu, '$1 $2').replace(/\s{2,}/g, ' ').split(/\s+/).reduce((words, token) => {
|
|
66
|
+
if (words.stopped || words.list.length >= 4) return { ...words, stopped: true };
|
|
67
|
+
const clean = token.replace(/^[«»"'„“]+|[«»"'„“!|,]+$/g, '');
|
|
68
|
+
if (!clean || RC_STOP_WORD_RE.test(clean) || RC_ATTRIBUTE_TOKEN_RE.test(clean)) return { ...words, stopped: true, by: clean };
|
|
69
|
+
return { list: [...words.list, clean], stopped: false, by: words.by };
|
|
70
|
+
}, { list: [], stopped: false, by: '' });
|
|
71
|
+
const words = RC_DISTRICT_MARKER_RE.test(parsed.by) ? parsed.list.slice(0, -1) : parsed.list;
|
|
72
|
+
while (words.length > 1 && [...words[words.length - 1]].length <= 2) words.pop();
|
|
73
|
+
const name = words.join(' ').trim();
|
|
74
|
+
return /[a-zA-Zа-яёіїґ]{2,}/i.test(name) ? name : null;
|
|
75
|
+
}
|