@whiteslove/parsing-lexicon 0.2.4 → 0.2.6
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 +16 -0
- package/src/housing-structured.js +4 -3
- package/src/housing-text.d.ts +3 -0
- package/src/housing-text.js +24 -0
- package/src/housing.js +29 -3
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
|
@@ -130,3 +130,19 @@ export function resolveHousingIntent(value) {
|
|
|
130
130
|
dealType: durationDeal.canonical,
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
export function classifyHousingDealType(value) {
|
|
135
|
+
const resolved = resolveHousingIntent(value)?.dealType;
|
|
136
|
+
if (resolved) return resolved;
|
|
137
|
+
|
|
138
|
+
// Some housing feeds omit an explicit rent verb but still use established
|
|
139
|
+
// Uzbek offer/share shorthand. Keep this fallback package-owned so consumers
|
|
140
|
+
// do not carry their own multilingual regexes.
|
|
141
|
+
const text = String(value || '');
|
|
142
|
+
if (!text) return null;
|
|
143
|
+
if (/(?:sherik(?:ka|lik)|шерик(?:ка|лик)|(?:1|bir)\s+ta\s+qiz\s+sherik|(?:1|бир)\s*та\s*(?:бола|киши|қиз|киз)\s*керак|1\s*хонага[^\r\n]{0,40}(?:киши|одам)\s*турилади|oila(?:ga)?\s+qo['’`]?yiladi|oila(?:ga)?\s+quyiladi|(?:хонали|квартир)[^\r\n]{0,100}турибди[^\r\n]{0,30}\d+\s*\$|квартира\s+бор)/iu.test(text)) {
|
|
144
|
+
return 'longRent';
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
@@ -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
|
@@ -2,3 +2,6 @@ export declare function parseHousingRoomsFromText(value: unknown): number | null
|
|
|
2
2
|
export declare function parseHousingResidentialComplex(value: unknown): string | null;
|
|
3
3
|
export declare function parseHousingAreaFromText(value: unknown): number | null;
|
|
4
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
|
@@ -135,3 +135,27 @@ export function parseHousingFloorFromText(value) {
|
|
|
135
135
|
return { floor: null, totalFloors: null };
|
|
136
136
|
}
|
|
137
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: ['бөлме'],
|
|
@@ -119,8 +120,8 @@ export const CURRENCIES = Object.freeze([
|
|
|
119
120
|
|
|
120
121
|
export const SELLER_TERMS = Object.freeze({
|
|
121
122
|
owner: group('owner', {
|
|
122
|
-
ru: ['собственник', 'хозяин', 'от хозяина', 'без посредников'], en: ['owner', 'direct owner', 'no agent'], uk: ['власник', 'власниця', 'від власника', 'без посередників'], ro: ['proprietar', 'direct proprietar', 'fără agenție', 'fara agentie', 'fără intermediari'],
|
|
123
|
-
uzLatn: ['egasi', 'uy egasi', 'mulkdor', 'maklersiz', 'vositachisiz'], uzCyrl: ['эгаси', 'уй эгаси', 'мулкдор', 'маклерсиз', 'воситачисиз'], kk: ['иесі', 'үй иесі', 'меншік иесі', 'делдалсыз'],
|
|
123
|
+
ru: ['собственник', 'хозяин', 'от хозяина', 'без посредников', 'без посредника', 'без риелтора', 'без риэлтора', 'без маклера', 'без маклер', 'без агента'], en: ['owner', 'direct owner', 'no agent', 'no broker', 'no realtor'], uk: ['власник', 'власниця', 'від власника', 'без посередників'], ro: ['proprietar', 'direct proprietar', 'fără agenție', 'fara agentie', 'fără intermediari'],
|
|
124
|
+
uzLatn: ['egasi', 'uy egasi', 'mulkdor', 'maklersiz', 'vositachisiz'], uzCyrl: ['эгаси', 'уй эгаси', 'мулкдор', 'маклерсиз', 'воситачисиз', 'без маклер'], kk: ['иесі', 'үй иесі', 'меншік иесі', 'делдалсыз'],
|
|
124
125
|
}),
|
|
125
126
|
agency: group('agency', {
|
|
126
127
|
ru: ['агентство', 'агент', 'риелтор', 'риэлтор', 'маклер', 'посредник'], en: ['agency', 'agent', 'realtor', 'broker'], uk: ['агентство', 'агент', 'рієлтор', 'риелтор', 'посередник'], ro: ['agenție', 'agentie', 'agent imobiliar', 'agenție imobiliară', 'agentie imobiliara', 'broker'],
|
|
@@ -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
|
+
|