@whiteslove/parsing-lexicon 0.2.3 → 0.2.5
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 +7 -1
- package/package.json +1 -1
- package/src/contact.d.ts +2 -0
- package/src/contact.js +22 -0
- package/src/housing-intent.d.ts +2 -0
- package/src/housing-intent.js +5 -0
- package/src/housing-structured.js +4 -3
- package/src/housing-text.d.ts +5 -0
- package/src/housing-text.js +86 -0
- package/src/housing.js +27 -1
package/index.d.ts
CHANGED
|
@@ -372,4 +372,10 @@ export function parseExperience(value: unknown): ExperienceParseResult | null;
|
|
|
372
372
|
export * from './src/housing-context.js';
|
|
373
373
|
export * from './src/hiring-context.js';
|
|
374
374
|
export * from './src/housing-intent.js';
|
|
375
|
-
export * from './src/housing-structured.js';
|
|
375
|
+
export * from './src/housing-structured.js';
|
|
376
|
+
|
|
377
|
+
// housing semantic helper declarations added in 0.2.5
|
|
378
|
+
export function resolveHousingOccupancy(value: unknown): 'wholeProperty' | 'room' | 'sharedRoom' | 'bedSpace' | null;
|
|
379
|
+
export function looksHousingRoomOnly(value: unknown): boolean;
|
|
380
|
+
export function resolveHousingPropertyType(value: unknown): 'flat' | 'house' | 'room' | 'studio' | 'townhouse' | 'dormitory' | null;
|
|
381
|
+
|
package/package.json
CHANGED
package/src/contact.d.ts
CHANGED
|
@@ -42,3 +42,5 @@ export function parsePhoneNumbers(value: unknown, options?: ParsePhoneOptions):
|
|
|
42
42
|
export function normalizePhone(value: unknown, options?: ParsePhoneOptions): ParsedPhoneNumber | null;
|
|
43
43
|
export function findTelegramContacts(value: unknown): readonly TelegramContact[];
|
|
44
44
|
export function normalizeTelegramContact(value: unknown): TelegramContact | null;
|
|
45
|
+
export declare function parsePrimaryContact(value: unknown): string | null;
|
|
46
|
+
|
package/src/contact.js
CHANGED
|
@@ -157,3 +157,25 @@ export function findTelegramContacts(value) {
|
|
|
157
157
|
export function normalizeTelegramContact(value) {
|
|
158
158
|
return findTelegramContacts(value)[0] || null;
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
export function parsePrimaryContact(value) {
|
|
162
|
+
const text = String(value || '');
|
|
163
|
+
if (!text) return null;
|
|
164
|
+
const intl = text.match(/\+\d[\d\s().-]{7,}\d/);
|
|
165
|
+
if (intl) {
|
|
166
|
+
const digits = intl[0].replace(/\D/g, '');
|
|
167
|
+
if (digits.length >= 10 && digits.length <= 15) return `+${digits}`;
|
|
168
|
+
}
|
|
169
|
+
const keyword = text.match(/(?:tel|тел|phone|моб|whats?app|viber|telegram|звонит|звоніть|aloqa|byla|contact)[^\d+]{0,8}(\+?\d[\d\s().-]{6,}\d)/iu);
|
|
170
|
+
if (keyword) {
|
|
171
|
+
const digits = keyword[1].replace(/\D/g, '');
|
|
172
|
+
if (digits.length >= 9 && digits.length <= 15) return keyword[1].trim();
|
|
173
|
+
}
|
|
174
|
+
const trailing = text.match(/(\+?\d[\d\s().-]{6,}\d)\s*(?:tel|тел(?:ефон)?|phone|моб|whats?app|viber|telegram|aloqa|contact)(?=$|[^\p{L}\p{N}_])/iu);
|
|
175
|
+
if (trailing) {
|
|
176
|
+
const digits = trailing[1].replace(/\D/g, '');
|
|
177
|
+
if (digits.length >= 9 && digits.length <= 15) return trailing[1].trim();
|
|
178
|
+
}
|
|
179
|
+
return findTelegramContacts(text)[0]?.handle || null;
|
|
180
|
+
}
|
|
181
|
+
|
package/src/housing-intent.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export const HOUSING_INTENT: readonly unknown[];
|
|
|
7
7
|
export const HOUSING_DEAL_TYPES: readonly unknown[];
|
|
8
8
|
export const HOUSING_ACTION_MAP: Readonly<Record<HousingAction, Readonly<{ listingKind: HousingListingKind; dealType: Exclude<HousingDealType, 'shortRent'> }>>>;
|
|
9
9
|
export function resolveHousingIntent(value: unknown): HousingIntentResult | null;
|
|
10
|
+
export declare function classifyHousingDealType(value: unknown): 'sale' | 'longRent' | 'shortRent' | null;
|
|
11
|
+
|
package/src/housing-intent.js
CHANGED
|
@@ -127,8 +127,9 @@ export function parseHousingPayments(value) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const noCommission = SELLER_TERMS.noCommission && findCanonical(text, [SELLER_TERMS.noCommission], { partial: true });
|
|
130
|
-
const
|
|
131
|
-
const
|
|
130
|
+
const shorthandCommission = text.match(/(?:^|[^\p{L}\p{N}_])[mм]\s*[:.\-]?\s*(\d{1,3})\s*%/iu);
|
|
131
|
+
const commissionPercent = toNumber(shorthandCommission?.[1] ?? text.match(/(?:комисси\p{L}*|commission|comision|komissiya|маклер|makler|rieltor|vositachi)[^\d%]{0,16}(\d{1,3}(?:[.,]\d+)?)\s*%/iu)?.[1]);
|
|
132
|
+
const commissionMentioned = Boolean(shorthandCommission) || (SELLER_TERMS.commission && findCanonical(text, [SELLER_TERMS.commission], { partial: true }));
|
|
132
133
|
|
|
133
134
|
return deepFreeze({
|
|
134
135
|
deposit: {
|
|
@@ -150,7 +151,7 @@ export function parseHousingSeller(value) {
|
|
|
150
151
|
const text = normalizeUnicode(value ?? '');
|
|
151
152
|
if (!text) return deepFreeze({ type: null, confidence: 0 });
|
|
152
153
|
const owner = SELLER_TERMS.owner && findCanonical(text, [SELLER_TERMS.owner], { partial: true });
|
|
153
|
-
const agency = SELLER_TERMS.agency && findCanonical(text, [SELLER_TERMS.agency], { partial: true });
|
|
154
|
+
const agency = (SELLER_TERMS.agency && findCanonical(text, [SELLER_TERMS.agency], { partial: true })) || /(?:^|[^\p{L}\p{N}_])[mм]\s*\d{1,3}\s*%/iu.test(text);
|
|
154
155
|
if (owner && !agency) return deepFreeze({ type: 'owner', confidence: 1 });
|
|
155
156
|
if (agency && !owner) return deepFreeze({ type: 'agency', confidence: 1 });
|
|
156
157
|
if (owner && agency) return deepFreeze({ type: null, confidence: 0.45 });
|
package/src/housing-text.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export declare function parseHousingRoomsFromText(value: unknown): number | null;
|
|
2
2
|
export declare function parseHousingResidentialComplex(value: unknown): string | null;
|
|
3
|
+
export declare function parseHousingAreaFromText(value: unknown): number | null;
|
|
4
|
+
export declare function parseHousingFloorFromText(value: unknown): { floor: number | null; totalFloors: number | null };
|
|
5
|
+
export declare function parseHousingAudience(value: unknown): 'family' | 'women' | 'men' | null;
|
|
6
|
+
export declare function parseHousingAmenities(value: unknown): readonly ('dishwasher' | 'separateRooms' | 'washingMachine' | 'television' | 'bedLinen' | 'towels')[];
|
|
7
|
+
|
package/src/housing-text.js
CHANGED
|
@@ -73,3 +73,89 @@ export function parseHousingResidentialComplex(value) {
|
|
|
73
73
|
const name = words.join(' ').trim();
|
|
74
74
|
return /[a-zA-Zа-яёіїґ]{2,}/i.test(name) ? name : null;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
export function parseHousingAreaFromText(value) {
|
|
78
|
+
const text = String(value || '');
|
|
79
|
+
if (!text) return null;
|
|
80
|
+
const match =
|
|
81
|
+
text.match(/(\d{2,4})\s*(?:m2|m²|мкв|м2|м²|sq ?m|кв\.?\s*м|квадрат[а-яё]*)/i) ||
|
|
82
|
+
text.match(/(?:^|\n)[^\d\r\n]{0,8}[1-9]\s*[¹²³⁴⁵⁶⁷⁸⁹]?\s*\/\s*[0-9]{1,2}\s*\/\s*[0-9]{1,2}\s+(\d{2,4})\s*кв(?=\s|$)/im);
|
|
83
|
+
return match ? Number(match[1]) : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseHousingFloorFromText(value) {
|
|
87
|
+
const text = String(value || '');
|
|
88
|
+
if (!text) return { floor: null, totalFloors: null };
|
|
89
|
+
const t = text.toLowerCase();
|
|
90
|
+
const floorWord = '(?:этаж(?:да)?|поверх|qavat|қабат|қабатт|etaj|floor|эт\\.)';
|
|
91
|
+
const valid = (floor, total) => floor >= 0 && floor <= 200 && (total == null || (total >= floor && total <= 200));
|
|
92
|
+
|
|
93
|
+
if (/(?:^|\n)[^\d\r\n]{0,8}[1-9]\s*\/\s*0\s*\/\s*-1\s*(?:этаж|эт\.?)?[^\r\n]*(?:подвал|цоколь)/im.test(t)) return { floor: -1, totalFloors: null };
|
|
94
|
+
|
|
95
|
+
const compact = t.match(/(?:^|\n)[^\d\r\n]{0,8}[1-9]\s*[¹²³⁴⁵⁶⁷⁸⁹]?\s*\/\s*([0-9]{1,2})\s*\/\s*([0-9]{1,2})[.,;:]?(?=\s|$)/m);
|
|
96
|
+
if (compact) {
|
|
97
|
+
const floor = Number(compact[1]);
|
|
98
|
+
const total = Number(compact[2]);
|
|
99
|
+
if (floor >= 1 && floor <= 40 && total >= 2 && total <= 40 && floor <= total) return { floor, totalFloors: total };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const labelledPair = t.match(/([1-9]\d?)\s*-?\s*(?:qavat|этаж|поверх|қабат)\s*(?:\/|из|iz|of)\s*([1-9]\d?)\s*-?\s*(?:qavatli|qavat|этаж(?:ей|ный)?|поверх(?:ів|овий)?|қабатты?)/i);
|
|
103
|
+
if (labelledPair) {
|
|
104
|
+
const floor = Number(labelledPair[1]);
|
|
105
|
+
const total = Number(labelledPair[2]);
|
|
106
|
+
if (floor <= 40 && total <= 40 && floor <= total) return { floor, totalFloors: total };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sep = '(?:\\/|из|iz|of)';
|
|
110
|
+
const pair = t.match(new RegExp(`${floorWord}\\D{0,4}(\\d{1,2})\\s*${sep}\\s*(\\d{1,2})`)) || t.match(new RegExp(`(\\d{1,2})\\s*${sep}\\s*(\\d{1,2})\\s*${floorWord}`));
|
|
111
|
+
if (pair) {
|
|
112
|
+
const floor = Number(pair[1]);
|
|
113
|
+
const total = Number(pair[2]);
|
|
114
|
+
if (valid(floor, total)) return { floor, totalFloors: total };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const notLetter = '(?!н|ей|ів|ност|ка|ки|s)';
|
|
118
|
+
const single = t.match(new RegExp(`(\\d{1,2})[^\\S\\r\\n]*-?[^\\S\\r\\n]*(?:го|ом|ым|ой|ий|nd|rd|th|st|й|м|е)?[^\\S\\r\\n]*${floorWord}${notLetter}`)) || t.match(new RegExp(`${floorWord}\\s*[:№#]?\\s*(\\d{1,2})\\b`));
|
|
119
|
+
if (single) {
|
|
120
|
+
const floor = Number(single[1]);
|
|
121
|
+
if (valid(floor, null)) {
|
|
122
|
+
const explicitTotal = t.match(/(?:этажность|этажей|поверхови|поверховість|qavatlar(?:\s*soni)?|qavatli|қабатты?)\D{0,6}(\d{1,2})/);
|
|
123
|
+
const leadingTotal = t.match(/([1-9]\d?)\s*-?\s*(?:этажн[а-яё]*|поверхов[а-яіїґ]*|qavatli|қабатты?)\s*(?:дом|здани|будин|uy|bino)?/i);
|
|
124
|
+
const total = explicitTotal ? Number(explicitTotal[1]) : leadingTotal ? Number(leadingTotal[1]) : null;
|
|
125
|
+
return { floor, totalFloors: total && total >= floor && total <= 200 ? total : null };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const bare = t.match(/(?<![\d/])([1-9]\d?)\s*\/\s*([1-9]\d?)(?![\d/])/);
|
|
130
|
+
if (bare) {
|
|
131
|
+
const floor = Number(bare[1]);
|
|
132
|
+
const total = Number(bare[2]);
|
|
133
|
+
if (floor >= 1 && floor <= 40 && total >= 2 && total <= 40 && floor <= total) return { floor, totalFloors: total };
|
|
134
|
+
}
|
|
135
|
+
return { floor: null, totalFloors: null };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function parseHousingAudience(value) {
|
|
139
|
+
const text = String(value || '');
|
|
140
|
+
if (!text) return null;
|
|
141
|
+
const t = text.toLowerCase();
|
|
142
|
+
if (/(?:семейн|сімейн|для семь|для сім)[^.\n]{0,80}(?:одиноч|мужчин|женщин|чоловік|жінок)|(?:одиноч|мужчин|женщин|чоловік|жінок)[^.\n]{0,80}(?:семейн|сімейн|для семь|для сім)/u.test(t)) return null;
|
|
143
|
+
if (/(для семь|семейн|сімейн|для сім|для родин|for famil|families?|pentru famil|oila(?:ga| uchun|\s+qo['’`]?yiladi|\s+quyiladi)|оила|отбасы)/u.test(t)) return 'family';
|
|
144
|
+
if (/(девуш|девоч|для дівч|дівчат|for girls|for women|only girls|doar fete|\bfete\b|qiz(?:lar|la)?(?:ga| uchun)?|(?:қ|к)из(?:лар|ла)?|қыздар)/u.test(t)) return 'women';
|
|
145
|
+
if (/(парн(ей|ям)|для мужчин|мужчинам|для хлопц|for men\b|for boys|doar b[aă]ie[țt]i|yigit(lar)?(ga| uchun)?|(?:ў|у)гил\s*бол|жігіт|ер адам)/u.test(t)) return 'men';
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function parseHousingAmenities(value) {
|
|
150
|
+
const text = String(value || '');
|
|
151
|
+
if (!text) return Object.freeze([]);
|
|
152
|
+
const amenities = [];
|
|
153
|
+
if (/(?:посудомо|посудомийн|dishwasher|idish\s*yuvish|idishyuvg|ma[șs]ina de sp[ăa]lat vase)/iu.test(text)) amenities.push('dishwasher');
|
|
154
|
+
if (/(?:комнат\p{L}*\s+раздельн|изолированн\p{L}*\s+комнат|separate\s+rooms?)/iu.test(text)) amenities.push('separateRooms');
|
|
155
|
+
if (/(?:стиральн\p{L}*\s+машин|washing\s+machine|kir\s*yuvish\s*mashin|kirmoshina)/iu.test(text)) amenities.push('washingMachine');
|
|
156
|
+
if (/(?:телевизор|телевизион|televizor|television|\btv\b)/iu.test(text)) amenities.push('television');
|
|
157
|
+
if (/(?:постельн\p{L}*\s+бель|bed\s*linen|toza\s+choyshab|yostiq\s+jild)/iu.test(text)) amenities.push('bedLinen');
|
|
158
|
+
if (/(?:полотенц|towels?|sochiq)/iu.test(text)) amenities.push('towels');
|
|
159
|
+
return Object.freeze(amenities);
|
|
160
|
+
}
|
|
161
|
+
|
package/src/housing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lexiconEntity } from './lexicon-core.js';
|
|
2
|
+
import { findCanonical } from './normalization.js';
|
|
2
3
|
import { HOUSING_DEAL_TYPES } from './housing-intent.js';
|
|
3
4
|
const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
|
|
4
5
|
|
|
@@ -76,7 +77,7 @@ export const PROPERTY_TYPES = Object.freeze([
|
|
|
76
77
|
}),
|
|
77
78
|
group('house', {
|
|
78
79
|
ru: ['дом', 'частный дом', 'коттедж'], en: ['house', 'home', 'cottage'], uk: ['будинок', 'приватний будинок', 'котедж'], ro: ['casă', 'casa', 'vilă', 'vila'],
|
|
79
|
-
uzLatn: ['
|
|
80
|
+
uzLatn: ['hovli', 'xovli'], uzCyrl: ['ҳовли', 'ховли'], kk: ['үй', 'жеке үй', 'коттедж'],
|
|
80
81
|
}),
|
|
81
82
|
group('room', {
|
|
82
83
|
ru: ['комната', 'комнату'], en: ['room'], uk: ['кімната'], ro: ['cameră', 'camera'], uzLatn: ['xona', 'hona'], uzCyrl: ['хона'], kk: ['бөлме'],
|
|
@@ -198,3 +199,28 @@ export function flattenAliases(item) {
|
|
|
198
199
|
if (!item?.aliases) return [];
|
|
199
200
|
return Object.values(item.aliases).flat();
|
|
200
201
|
}
|
|
202
|
+
|
|
203
|
+
export function resolveHousingOccupancy(value) {
|
|
204
|
+
const match = findCanonical(value, HOUSING_OCCUPANCY_TYPES, { partial: true });
|
|
205
|
+
return match?.canonical || null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function looksHousingRoomOnly(value) {
|
|
209
|
+
const text = String(value || '');
|
|
210
|
+
if (!text) return false;
|
|
211
|
+
const occupancy = resolveHousingOccupancy(text);
|
|
212
|
+
if (occupancy === 'room' || occupancy === 'sharedRoom' || occupancy === 'bedSpace') return true;
|
|
213
|
+
return /подселени|підселен|комнату\s+в|кімнату\s+в|сда[её]тся\s+комната|сдается\s+комната|сдам\s+комнату|здам\s+кімнат|room\s+in\s+a\s+(?:shared\s+)?flat|room\s+for\s+rent|shared\s+(?:flat|apartment|room)|roommate|flatmate|xona\s+ijaraga|xona\s+beriladi|sherik(?:ka|lik)|шерик(?:ка|лик)|(?:1|бир)\s*та\s*(?:бола|киши|қиз|киз)\s*керак|1\s*хонага[^\r\n]{0,40}(?:киши|одам)\s*турилади|бөлме\s+жалға|închiriez\s+camer[ăa]|ищу[^\r\n]{0,60}сосед|ищем[^\r\n]{0,60}сосед|нужен[^\r\n]{0,60}сосед|нужна[^\r\n]{0,60}сосед|шукаю[^\r\n]{0,60}сусід|шукаємо[^\r\n]{0,60}сусід|потрібен[^\r\n]{0,60}сусід|потрібна[^\r\n]{0,60}сусід|співмешкан|співжител|соседк|сусідк/iu.test(text);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function resolveHousingPropertyType(value) {
|
|
217
|
+
const text = String(value || '');
|
|
218
|
+
if (!text) return null;
|
|
219
|
+
const flat = PROPERTY_TYPES.find((entry) => entry.canonical === 'flat');
|
|
220
|
+
if (flat && findCanonical(text, [flat], { partial: true })) return 'flat';
|
|
221
|
+
const genericUzbekHome = /(?:^|[^\p{L}\p{N}_])(?:uy|уй)(?=$|[^\p{L}\p{N}_])/iu.test(text);
|
|
222
|
+
const explicitHouse = /(?:hovli|xovli|ҳовли|ховли|house|casa|dom|villa|будин|коттедж|вілл|вилл|(?:^|[^\p{L}\p{N}_])(?:дом|үй)(?=$|[^\p{L}\p{N}_]))/iu.test(text);
|
|
223
|
+
if (genericUzbekHome && !explicitHouse) return null;
|
|
224
|
+
return findCanonical(text, PROPERTY_TYPES, { partial: true })?.canonical || null;
|
|
225
|
+
}
|
|
226
|
+
|