@whiteslove/parsing-lexicon 0.5.13 → 0.5.15

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/index.d.ts CHANGED
@@ -379,6 +379,7 @@ export * from './src/housing-title.js';
379
379
  export * from './src/housing-language.js';
380
380
  export * from './src/housing-features.js';
381
381
  export * from './src/housing-listing-fields.js';
382
+ export * from './src/housing-card-fields.js';
382
383
 
383
384
  // housing semantic helper declarations added in 0.2.5
384
385
  export function resolveHousingOccupancy(value: unknown): 'wholeProperty' | 'room' | 'sharedRoom' | 'bedSpace' | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -113,6 +113,10 @@
113
113
  "types": "./src/housing-listing-enrichment.d.ts",
114
114
  "import": "./src/housing-listing-enrichment.js"
115
115
  },
116
+ "./housing-card-fields": {
117
+ "types": "./src/housing-card-fields.d.ts",
118
+ "import": "./src/housing-card-fields.js"
119
+ },
116
120
  "./hiring": "./src/hiring.js",
117
121
  "./hiring-languages": "./src/hiring-languages.js",
118
122
  "./hiring-language-extensions": {
@@ -220,4 +224,4 @@
220
224
  "geography"
221
225
  ],
222
226
  "license": "MIT"
223
- }
227
+ }
@@ -0,0 +1,4 @@
1
+ export declare function parseHousingQuarterLabel(value: unknown): string | null;
2
+ export declare function parseHousingCardAmenities(value: unknown): readonly string[];
3
+ export declare function parseHousingNearbyMentions(value: unknown): readonly string[];
4
+ export declare function parseHousingNearbyShops(value: unknown): readonly string[];
@@ -0,0 +1,107 @@
1
+ import { deepFreeze } from './lexicon-core.js';
2
+ import { parseHousingAmenities } from './housing-text.js';
3
+
4
+ const AMENITY_LABELS = Object.freeze({
5
+ dishwasher: 'Dishwasher',
6
+ separateRooms: 'Separate rooms',
7
+ washingMachine: 'Washing machine',
8
+ television: 'Television',
9
+ bedLinen: 'Bed linen',
10
+ towels: 'Towels',
11
+ });
12
+
13
+ const STREET_LIKE_PLACE_RE = /(?:^|[^\p{L}\p{N}_])(?:ko['’`ʼʻ]?cha(?:si)?|кўча(?:си)?|yo['’`ʼʻ]?l(?:i)?|yoli|street|road|avenue|улица|вулиця|дорога|шоссе|проспект)(?=$|[^\p{L}\p{N}_])/iu;
14
+
15
+ const SHOP_CHAINS = Object.freeze([
16
+ ['Korzinka', /korzinka|корзинка/iu],
17
+ ['Makro', /(?:^|[^\p{L}\p{N}_])makro(?=$|[^\p{L}\p{N}_])|макро/iu],
18
+ ['Havas', /(?:^|[^\p{L}\p{N}_])[хxh]avas(?=$|[^\p{L}\p{N}_])|хавас/iu],
19
+ ['Carrefour', /carrefour|карфур/iu],
20
+ ['ATB', /(?:^|[^\p{L}\p{N}_])(?:атб|atb)(?=$|[^\p{L}\p{N}_])/iu],
21
+ ['Klass', /(?:^|[^\p{L}\p{N}_])(?:klass|класс)(?=$|[^\p{L}\p{N}_])/iu],
22
+ ['Magnum', /magnum|магнум/iu],
23
+ ['Bravo', /(?:^|[^\p{L}\p{N}_])bravo(?=$|[^\p{L}\p{N}_])|браво/iu],
24
+ ['Metro C&C', /(?:^|[^\p{L}\p{N}_])metro\s*(?:cash|c\s*&\s*c|market)|метро\s*кэш/iu],
25
+ ]);
26
+
27
+ const PLACE_KINDS = Object.freeze([
28
+ ['Рынок', /(?:рынок|базар|bozor)\s+([A-ZА-ЯЁ][\p{L}\p{N}_'’-]{2,24})/u],
29
+ ['ЗАГС', /(?:^|[^\p{L}\p{N}_])ЗАГС(?:\s+([A-ZА-ЯЁ][\p{L}\p{N}_'’-]{2,24}(?:\s+район\p{L}*)?))?/u],
30
+ ['Парк', /парк\s+([A-ZА-ЯЁ][\p{L}\p{N}_'’-]{2,24})/u],
31
+ ['Школа', /школа\s*(№\s*\d{1,4})/iu],
32
+ ['Клиника', /(?:клиника|поликлиника|больница)\s+([A-ZА-ЯЁ][\p{L}\p{N}_'’-]{2,24})/u],
33
+ ['Стадион', /стадион\s+([A-ZА-ЯЁ][\p{L}\p{N}_'’-]{2,24})/u],
34
+ ['Университет', /(?:университет|институт)\s+([A-ZА-ЯЁ][\p{L}\p{N}_'’-]{2,24})/u],
35
+ ]);
36
+
37
+ const NEARBY_BLOCK_RE = /(?:^|\n)[^\n]{0,30}?(?:что\s+(?:есть|находится)\s+р[яa]дом|р[яa]дом(?:\s+(?:есть|находятся|расположены|с\s+домом))?|непода[лл][её]ку|поблизости|в\s+шаговой\s+доступности|окружени[ие]|ориентир(?:ы|ами)?|yaqinida|atrofida|yonida|nearby|landmarks?)\s*[:—-]?\s*([^\n]{4,500})/iu;
38
+ const NEARBY_NOISE_RE = /^(?:и|а|в|на|у|до|от|все|вс[её]|есть|рядом|близко|недалеко|минут\p{L}*|пешком|транспорт|остановк\p{L}*|магазин|магазины|аптека|аптеки|садик|садики|всё\s+необходимое|развит\p{L}*)$/iu;
39
+ const NEARBY_FILLER_RE = /(?:инфраструктур\p{L}*|все\s+необходимо|вс[её]\s+рядом|шаговой\s+доступност|развитый\s+район)/iu;
40
+
41
+ function enumeratedNearby(text) {
42
+ const blocks = [...text.matchAll(new RegExp(NEARBY_BLOCK_RE.source, `${NEARBY_BLOCK_RE.flags}g`))]
43
+ .map((match) => match[1])
44
+ .filter(Boolean);
45
+ const seen = new Set();
46
+ const items = [];
47
+ for (const item of blocks.join(', ').split(/[,;•·|]+/).map((part) => part
48
+ .replace(/\s{2,}/g, ' ')
49
+ .replace(/^[\s\-–—.]+|[\s\-–—.!]+$/g, '')
50
+ .trim())) {
51
+ if (item.length < 3 || item.length > 45 || NEARBY_NOISE_RE.test(item) || NEARBY_FILLER_RE.test(item)) continue;
52
+ if (!/\p{L}{3,}/u.test(item)) continue;
53
+ const key = item.toLocaleLowerCase();
54
+ if (seen.has(key)) continue;
55
+ seen.add(key);
56
+ items.push(item);
57
+ if (items.length >= 14) break;
58
+ }
59
+ return items;
60
+ }
61
+
62
+ export function parseHousingQuarterLabel(value) {
63
+ const text = String(value || '');
64
+ if (!text) return null;
65
+ if (/(?:^|[^\p{L}\p{N}_])(?:глинк[аи]?|glinka)(?:$|[^\p{L}\p{N}_])/iu.test(text)) return 'Glinka';
66
+ const match = text.match(/(\d{1,3})\s*(?:-?\s*(?:chi|чи))?\s*[-\s]?\s*(?:квартал|кв-?л(?=$|[^\p{L}\p{N}_])|kvartal(?:i)?|квартил|kvartil|мкр(?=$|[^\p{L}\p{N}_])|микрорайон|массив|massiv|daha(?:si|dan)?|hudud|худуд)/iu)
67
+ || text.match(/(?:квартал|kvartal(?:i)?|квартил|kvartil|мкр|микрорайон|массив|massiv|daha(?:si|dan)?|hudud|худуд)\s*[-№#]?\s*(\d{1,3})/iu)
68
+ || text.match(/(?:чиланзар|chilonzor|chilanzar)\s*[-№#]?\s*(\d{1,2})(?!\d)/iu);
69
+ if (match) {
70
+ const label = `${Number(match[1])} kvartal`;
71
+ return STREET_LIKE_PLACE_RE.test(label) ? null : label;
72
+ }
73
+ const centralBlock = text.match(/(?:^|[^\p{L}\p{N}_])(?:ц|c)\s*[-–]?\s*(\d{1,2})(?:$|[^\p{L}\p{N}_])/iu);
74
+ return centralBlock ? `C-${Number(centralBlock[1])}` : null;
75
+ }
76
+
77
+ export function parseHousingCardAmenities(value) {
78
+ const amenities = parseHousingAmenities(value)
79
+ .filter((name) => AMENITY_LABELS[name])
80
+ .map((name) => AMENITY_LABELS[name]);
81
+ return deepFreeze(amenities);
82
+ }
83
+
84
+ export function parseHousingNearbyMentions(value) {
85
+ const text = String(value || '');
86
+ if (!text) return deepFreeze([]);
87
+ const out = enumeratedNearby(text);
88
+ for (const [kind, pattern] of PLACE_KINDS) {
89
+ const match = text.match(pattern);
90
+ if (!match) continue;
91
+ const label = match[1] ? `${kind} ${match[1].trim()}` : kind;
92
+ if (out.some((item) => item.toLocaleLowerCase().includes(kind.toLocaleLowerCase()))) continue;
93
+ if (!out.includes(label)) out.push(label);
94
+ }
95
+ return deepFreeze(out.slice(0, 16));
96
+ }
97
+
98
+ export function parseHousingNearbyShops(value) {
99
+ const text = String(value || '');
100
+ if (!text) return deepFreeze([]);
101
+ const out = SHOP_CHAINS.filter(([, pattern]) => pattern.test(text)).map(([name]) => name);
102
+ const namedMall = text.match(/([A-Za-z][A-Za-z'’.&-]*(?:\s+[A-Za-z][A-Za-z'’.&-]*){0,3}\s+(?:mall|moll|молл))/iu)?.[1]?.trim();
103
+ if (namedMall && !out.includes(namedMall)) out.push(namedMall);
104
+ const shoppingCenter = text.match(/(?:трц|тц)\s+([A-Za-z0-9'’.-]{2,25}|[А-Яа-яЁё0-9'’.-]{2,25})/iu)?.[1]?.trim();
105
+ if (shoppingCenter && !out.includes(`ТРЦ ${shoppingCenter}`)) out.push(`ТРЦ ${shoppingCenter}`);
106
+ return deepFreeze(out);
107
+ }
@@ -7,7 +7,7 @@ import { GENERIC_LANDMARK_TERMS } from './landmarks.js';
7
7
  import { LOCATION_RELATIONS, parseHousingContext } from './housing-context.js';
8
8
  import { resolveHousingIntent } from './housing-intent.js';
9
9
  import { countryCurrency, countryPhoneHint } from './country-context.js';
10
- import { findTelegramContacts, parsePhoneNumbers } from './contact.js';
10
+ import { findTelegramContacts, maskPhoneLikeSpans, parsePhoneNumbers } from './contact.js';
11
11
  import { parseHousingAddress } from './housing-address.js';
12
12
  import { parseHousingListingFields } from './housing-listing-fields.js';
13
13
  import { parseHousingPrice } from './housing-money.js';
@@ -167,8 +167,9 @@ export function parseHousingAreas(value) {
167
167
  }
168
168
 
169
169
  function amountAroundKeyword(text, keywordRe) {
170
- const after = text.match(new RegExp(`${keywordRe.source}[^\\d$€₴₸]{0,24}([$€₴₸])?\\s*(\\d{1,9}(?:[.,]\\d{1,2})?)\\s*([\\p{L}.'$€₴₸]{0,12})`, 'iu'));
171
- const before = text.match(new RegExp(`([$€₴₸])?\\s*(\\d{1,9}(?:[.,]\\d{1,2})?)\\s*([\\p{L}.']{0,12})[^\\d]{0,16}${keywordRe.source}`, 'iu'));
170
+ const value = maskPhoneLikeSpans(text);
171
+ const after = value.match(new RegExp(`${keywordRe.source}[^\\d$€₴₸\\r\\n]{0,24}([$€₴₸])?\\s*(\\d{1,3}(?:[ \\u00a0]\\d{3})+|\\d{1,9}(?:[.,]\\d{1,2})?)\\s*([\\p{L}.'$€₴₸]{0,12})`, 'iu'));
172
+ const before = value.match(new RegExp(`([$€₴₸])?\\s*(\\d{1,3}(?:[ \\u00a0]\\d{3})+|\\d{1,9}(?:[.,]\\d{1,2})?)\\s*([\\p{L}.']{0,12})[^\\d\\r\\n]{0,16}${keywordRe.source}`, 'iu'));
172
173
  const match = after || before;
173
174
  if (!match) return { amount: null, currency: null };
174
175
  const amount = toNumber(match[2]);
@@ -178,7 +178,7 @@ export function parseHousingAmenities(value) {
178
178
  if (!text) return Object.freeze([]);
179
179
  const amenities = [];
180
180
  if (/(?:посудомо|посудомийн|dishwasher|idish\s*yuvish|idishyuvg|ma[șs]ina de sp[ăa]lat vase)/iu.test(text)) amenities.push('dishwasher');
181
- if (/(?:комнат\p{L}*\s+раздельн|изолированн\p{L}*\s+комнат|separate\s+rooms?)/iu.test(text)) amenities.push('separateRooms');
181
+ if (/(?:комнат\p{L}*\s+раздельн|(?:раздельн|изолированн)\p{L}*\s+комнат|separate\s+rooms?)/iu.test(text)) amenities.push('separateRooms');
182
182
  if (/(?:кроват|\bbed\b|\bspalni\b|\bsplani\b|yotoq\s+karavot|ётоқ\s+каравот)/iu.test(text)) amenities.push('bed');
183
183
  if (/(?:стиральн\p{L}*\s+машин|washing\s+machine|kir\s*yuvish\s*mashin|kirmoshina|кир\s+ювиш\s+машина)/iu.test(text)) amenities.push('washingMachine');
184
184
  if (/(?:холодильник|refrigerator|fridge|muzlatgich|музлатгич|xolodilnik)/iu.test(text)) amenities.push('refrigerator');
package/src/index.js CHANGED
@@ -47,6 +47,7 @@ export * from './housing-text.js';
47
47
  export * from './housing-poi-extensions.js';
48
48
  export * from './housing-display.js';
49
49
  export * from './housing-listing-enrichment.js';
50
+ export * from './housing-card-fields.js';
50
51
  export * from './hiring.js';
51
52
  export * from './hiring-languages.js';
52
53
  export * from './hiring-context.js';