@whiteslove/parsing-lexicon 0.6.6 → 0.6.8
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
|
@@ -20,7 +20,9 @@ const SHOP_CHAINS = Object.freeze([
|
|
|
20
20
|
['Havas', /(?:^|[^\p{L}\p{N}_])[хxh]avas(?=$|[^\p{L}\p{N}_])|хавас/iu],
|
|
21
21
|
['Carrefour', /carrefour|карфур/iu],
|
|
22
22
|
['ATB', /(?:^|[^\p{L}\p{N}_])(?:атб|atb)(?=$|[^\p{L}\p{N}_])/iu],
|
|
23
|
-
|
|
23
|
+
// Hyphen excluded on both sides: the chain is a shop name, while
|
|
24
|
+
// "бизнес-класс" / "эконом-класс" / "премиум-класс" describe the housing.
|
|
25
|
+
['Klass', /(?:^|[^\p{L}\p{N}_-])(?:klass|класс)(?=$|[^\p{L}\p{N}_-])/iu],
|
|
24
26
|
['Magnum', /magnum|магнум/iu],
|
|
25
27
|
['Bravo', /(?:^|[^\p{L}\p{N}_])bravo(?=$|[^\p{L}\p{N}_])|браво/iu],
|
|
26
28
|
['Metro C&C', /(?:^|[^\p{L}\p{N}_])metro\s*(?:cash|c\s*&\s*c|market)|метро\s*кэш/iu],
|
|
@@ -27,6 +27,11 @@ const NUMBER_WORDS = Object.freeze([
|
|
|
27
27
|
[/(?<![\p{L}\p{N}_])(?:десятикомнатн\p{L}*|o['’]?n\s+xona(?:li)?|on\s+xona(?:li)?|ўн\s+хона(?:ли|лик)?|он\s+хона(?:ли|лик)?|10\s*(?:-\s*)?к(?:омн\p{L}*)?|10\s*(?:-\s*)?xona(?:li)?|10\s*(?:-\s*)?хона(?:лик|ли)?|ten[- ]bedroom|ten[- ]room)(?![\p{L}\p{N}_])/iu, 10],
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
|
+
// Common classifieds shorthand: rooms/floor/total floors, optionally followed
|
|
31
|
+
// by the area ("2/10/16", "1/5/12-50m"). Requiring the shorthand to start its
|
|
32
|
+
// own line avoids reading dates in ordinary prose as housing data.
|
|
33
|
+
const COMPACT_SPEC_RE = /(?:^|[\r\n])[^\S\r\n]*(\d{1,2})[^\S\r\n]*[/\\][^\S\r\n]*(\d{1,3})[^\S\r\n]*[/\\][^\S\r\n]*(\d{1,3})(?:[^\S\r\n]*[-–—][^\S\r\n]*(\d{1,4}(?:[.,]\d{1,2})?)[^\S\r\n]*(?:м²|м2|m²|m2|кв\.?[^\S\r\n]*м|sqm|м|m)?)?(?=$|\s)/u;
|
|
34
|
+
|
|
30
35
|
const FIRST_FLOOR_WORD_RE = /(?<![\p{L}\p{N}_])(?:перш(?:ий\s+поверх|ому\s+поверс(?:і|у))|(?:на\s+)?першому\s+поверсі|перв(?:ый\s+этаж|ом\s+этаже)|(?:на\s+)?первом\s+этаже)(?![\p{L}\p{N}_])/iu;
|
|
31
36
|
|
|
32
37
|
function toNumber(value) {
|
|
@@ -36,6 +41,27 @@ function toNumber(value) {
|
|
|
36
41
|
return Number.isFinite(number) ? number : null;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
// Reads the rooms/floor/total floors[-area] shorthand into its parts. All the
|
|
45
|
+
// range checks live here so the floor, room and area parsers share a single
|
|
46
|
+
// interpretation of the shorthand instead of each re-reading its numbers.
|
|
47
|
+
function parseCompactSpec(text) {
|
|
48
|
+
const match = text.match(COMPACT_SPEC_RE);
|
|
49
|
+
if (!match) return null;
|
|
50
|
+
const rooms = toNumber(match[1]);
|
|
51
|
+
const floor = toNumber(match[2]);
|
|
52
|
+
const totalFloors = toNumber(match[3]);
|
|
53
|
+
const areaSqm = toNumber(match[4]);
|
|
54
|
+
if (rooms == null || rooms < 1 || rooms > 20) return null;
|
|
55
|
+
if (floor == null || totalFloors == null) return null;
|
|
56
|
+
if (floor < 1 || floor > totalFloors || totalFloors > 200) return null;
|
|
57
|
+
return {
|
|
58
|
+
rooms,
|
|
59
|
+
floor,
|
|
60
|
+
totalFloors,
|
|
61
|
+
areaSqm: areaSqm != null && areaSqm > 0 && areaSqm <= 100000 ? areaSqm : null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
function currencyNear(text) {
|
|
40
66
|
// findCanonical's partial match requires the alias to be its own
|
|
41
67
|
// space-delimited token, so a currency symbol glued directly to its
|
|
@@ -77,31 +103,29 @@ export function parseHousingRoomCount(value) {
|
|
|
77
103
|
// put the label before the number: "Комнат - 4" instead of "4 комнаты".
|
|
78
104
|
const reversed = text.match(/(?:комнат\p{L}*|xona(?:lar)?(?:i)?|хона(?:лар)?(?:и|лик|ли)?|rooms?)\s*[-:–—]\s*(\d{1,2})(?=$|[^\p{L}\p{N}])/iu);
|
|
79
105
|
const reversedRooms = toNumber(reversed?.[1]);
|
|
80
|
-
|
|
106
|
+
if (reversedRooms != null && reversedRooms >= 1 && reversedRooms <= 20) return reversedRooms;
|
|
107
|
+
|
|
108
|
+
// Listings that only carry the compact "1/5/12-50m" shorthand state the room
|
|
109
|
+
// count in its first number.
|
|
110
|
+
return parseCompactSpec(text)?.rooms ?? null;
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
export function parseHousingFloor(value) {
|
|
84
114
|
const text = normalizeUnicode(value ?? '');
|
|
85
115
|
if (!text) return deepFreeze({ floor: null, totalFloors: null });
|
|
86
116
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (compactTriple) {
|
|
94
|
-
const rooms = toNumber(compactTriple[1]);
|
|
95
|
-
const floor = toNumber(compactTriple[2]);
|
|
96
|
-
const totalFloors = toNumber(compactTriple[3]);
|
|
97
|
-
if (rooms != null && rooms >= 1 && rooms <= 20
|
|
98
|
-
&& floor != null && totalFloors != null
|
|
99
|
-
&& floor >= 1 && floor <= totalFloors && totalFloors <= 200) {
|
|
100
|
-
return deepFreeze({ floor, totalFloors });
|
|
101
|
-
}
|
|
117
|
+
// The rooms/floor/total shorthand must be read before the generic
|
|
118
|
+
// floor/total fraction parser or its first two numbers would incorrectly
|
|
119
|
+
// become floor=2, totalFloors=10 for "2/10/16".
|
|
120
|
+
const compactSpec = parseCompactSpec(text);
|
|
121
|
+
if (compactSpec) {
|
|
122
|
+
return deepFreeze({ floor: compactSpec.floor, totalFloors: compactSpec.totalFloors });
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
|
|
125
|
+
// Skip a pair that is merely the head of a rooms/floor/total triple the
|
|
126
|
+
// shorthand parser above rejected: there the floor and the total floors are
|
|
127
|
+
// the second and third numbers, never the first two.
|
|
128
|
+
const fraction = text.match(/(?:^|[^\d])(\d{1,3})\s*[\/\\]\s*(\d{1,3})(?!\s*[\/\\]\s*\d)(?=$|[^\d])/u);
|
|
105
129
|
if (fraction) {
|
|
106
130
|
const floor = toNumber(fraction[1]);
|
|
107
131
|
const totalFloors = toNumber(fraction[2]);
|
|
@@ -176,6 +200,9 @@ export function parseHousingAreas(value) {
|
|
|
176
200
|
const generic = text.match(new RegExp(`(?:^|[^\\d])(\\d{1,4}(?:[.,]\\d{1,2})?)\\s*${AREA_UNIT_RE}(?=$|[^\\p{L}\\p{N}])`, 'iu'));
|
|
177
201
|
result.total = toNumber(generic?.[1]);
|
|
178
202
|
}
|
|
203
|
+
// The compact "1/5/12-50m" shorthand glues the area to the floor block, where
|
|
204
|
+
// the unit is often a bare "m" the generic area matcher cannot trust alone.
|
|
205
|
+
if (result.total == null) result.total = parseCompactSpec(text)?.areaSqm ?? null;
|
|
179
206
|
for (const key of Object.keys(result)) {
|
|
180
207
|
const number = result[key];
|
|
181
208
|
if (number != null && (number <= 0 || number > 100000)) result[key] = null;
|
|
@@ -150,6 +150,7 @@ export const TASHKENT_RESIDENTIAL_COMPLEXES = Object.freeze([
|
|
|
150
150
|
complex('Olmos Residence'),
|
|
151
151
|
complex('Olmazor City', ['Алмазар Сити', 'Олмазор Сити']),
|
|
152
152
|
complex('Orzu'),
|
|
153
|
+
complex('Orzu Saroyi', ['Орзу Саройи', 'Orzu Saroy', 'Орзу Сарой']),
|
|
153
154
|
complex('OQSAROY', ['ОК САРОЙ', 'Оксарой', 'Oq Saroy', 'Oq-Saroy']),
|
|
154
155
|
complex('O\'zbegim', ['Узбегим', 'O‘zbegim', 'Oʻzbegim', 'Ozbegim']),
|
|
155
156
|
complex('O\'z Mahal', ['Озмахал', 'Узмахал', 'O‘z Mahal', 'Oʻz Mahal', 'Oz Mahal']),
|