@whiteslove/parsing-lexicon 0.8.0 → 0.8.1
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
package/src/contact.js
CHANGED
|
@@ -9,6 +9,7 @@ const PHONE_LIKE_RE = /\+?\d(?:[\t \u00a0().-]*\d){9,}/g;
|
|
|
9
9
|
const PHONE_CANDIDATE_RE = /\+?\d(?:[\t \u00a0().-]*\d){6,}(?:[\t \u00a0]*(?:ext\.?|extension|x|доб\.?|дод\.?)\s*\d{1,6})?/giu;
|
|
10
10
|
const PHONE_EXTENSION_RE = /[\t \u00a0]*(?:ext\.?|extension|x|доб\.?|дод\.?)\s*(\d{1,6})$/iu;
|
|
11
11
|
const DATE_LIKE_PHONE_RE = /^\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})(?:\s+\d{1,2})?$/u;
|
|
12
|
+
const PRICE_LABEL_BEFORE_NUMBER_RE = /(?:цена|ціна|нарх(?:и)?|narx(?:i)?|price|стоимост[ьи]|аренд(?:а|ная\s+плата)?|rent)\s*[:=\-–—]?\s*$/iu;
|
|
12
13
|
|
|
13
14
|
const TELEGRAM_USERNAME_RE = /^[A-Za-z0-9_]{5,32}$/;
|
|
14
15
|
const TELEGRAM_LINK_RE = /(?:https?:\/\/)?(?:t\.me|telegram\.me|telegram\.dog)\/([A-Za-z0-9_]{5,32})(?:\/[0-9]+)?(?:[/?#][^\s]*)?/giu;
|
|
@@ -20,6 +21,18 @@ function normalizedCountryHint(value) {
|
|
|
20
21
|
return /^[A-Z]{2}$/.test(country) ? country : undefined;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// PHONE_LIKE_RE deliberately accepts punctuation-separated digit sequences.
|
|
25
|
+
// That also resembles a grouped monetary range such as
|
|
26
|
+
// "Narxi: 950 000 - 1.000.000". A nearby explicit price label is stronger
|
|
27
|
+
// semantic evidence than the broad (unvalidated) ten-digit phone mask, so do
|
|
28
|
+
// not hide that span before the money candidate parser sees it. This does
|
|
29
|
+
// not weaken validated national-phone parsing below.
|
|
30
|
+
function isExplicitPriceSpan(text, start, raw) {
|
|
31
|
+
if (!/[\-–—]/u.test(raw)) return false;
|
|
32
|
+
const before = text.slice(Math.max(0, start - 48), start);
|
|
33
|
+
return PRICE_LABEL_BEFORE_NUMBER_RE.test(before);
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
function splitPhoneExtension(raw) {
|
|
24
37
|
const match = String(raw || '').match(PHONE_EXTENSION_RE);
|
|
25
38
|
if (!match) return { base: String(raw || '').trim(), extension: null };
|
|
@@ -38,6 +51,7 @@ export function findPhoneLikeSpans(value, options = {}) {
|
|
|
38
51
|
const digits = raw.replace(/\D/g, '');
|
|
39
52
|
if (digits.length < 10) continue;
|
|
40
53
|
const start = match.index ?? 0;
|
|
54
|
+
if (isExplicitPriceSpan(text, start, raw)) continue;
|
|
41
55
|
spans.push(Object.freeze({
|
|
42
56
|
start,
|
|
43
57
|
end: start + raw.length,
|
|
@@ -32,11 +32,15 @@ const APPLIANCE_PATTERNS = Object.freeze([
|
|
|
32
32
|
const FIRST_RENT_UZ_RE = /(?:hali\s+hech\s+kim\s+(?:yashamagan|turmagan)|ҳали\s+ҳеч\s+ким\s+(?:яшамаган|турмаган))/iu;
|
|
33
33
|
const LANDLORD_PRESENT_RE = /(?:xozaykali|hojaykali|xo['’]?jaykali|с\s+хозяйк(?:ой|ой\s+в\s+квартире)|хозяйк\p{L}*\s+(?:жив[её]т|прожива\p{L}*)|with\s+(?:the\s+)?(?:landlord|owner)\s+(?:present|living\s+in)|cu\s+proprietar(?:ul)?\s+în\s+cas(?:ă|a)|үй\s*иесі\s+(?:тұрады|бірге\s+тұрады))/iu;
|
|
34
34
|
const STUDENT_RE = /(?:studentlar\s+uchun|talabalar\s+uchun|студент(?:ам|ы|ок|ов)?\s+(?:можно|для)|для\s+студент|students?\s+(?:only|welcome)|for\s+students|pentru\s+studen[țt]i|studen[țt]i(?:lor)?|студенттерге|студенттер\s+үшін|(?:oila|oyla)(?:ga|lar|li)?\s+yoki\s+\d{1,2}\s+ta\s+bola(?:lar)?(?:ga)?\s+(?:ijara(?:ga)?\s+)?(?:beril|topshiril))/iu;
|
|
35
|
-
const NO_BROKER_RE = /(?:bez\s
|
|
35
|
+
const NO_BROKER_RE = /(?:bez\s*makler|maklersiz|vositachisiz|без\s+(?:маклер|посредник|риелтор|риэлтор|комисси)|no\s+(?:broker|agent|commission|agency\s+fee)|f[ăa]r[ăa]\s+(?:comision|agen[țt]ie|intermediari)|делдалсыз|комиссиясыз)/iu;
|
|
36
36
|
const BROKER_RE = /(?:makler|vositachi|макл(?:ер[а-яё]*)?|ри[еэ]лтор[а-яё]*|агентств[а-яё]*|комисси[а-яё]*|broker|realtor|commission|comision(?:ul)?|agen[țt]ie|delda[lл]\p{L}*|делдал\p{L}*)/iu;
|
|
37
37
|
const MEN_RE = /(?:o['’ʻʼ‘`]?g['’ʻʼ‘`]?il\s+bola(?:lar)?(?:ga)?|ogil\s+bola(?:lar)?(?:ga)?|sherik\s+bola|эркак(?:лар)?|erkak(?:lar)?(?:ga)?|только\s+(?:мужчин|парн)|\bmen\s+only\b|b[ăa]rba[țt]i(?:lor)?|b[ăa]ie[țt]i(?:lor)?|жігіттерге|жігіттер(?:ге)?|хлопц(?:ям|і|ів)?|чоловік(?:ам|и)?)/iu;
|
|
38
38
|
const WOMEN_RE = /(?:qiz(?:lar)?(?:ga)?|ayol(?:lar)?(?:ga|ni)?|киз(?:лар)?(?:га)?|аёл(?:лар)?(?:га|ни)?|девушк\p{L}*|женщин\p{L}*|girls?\s+only|women\s+only|fete(?:lor)?|femei(?:lor)?|қыздарға|қыздар(?:ға)?|дівчат(?:ам|а|ок)?|жінк(?:ам|и)?)/iu;
|
|
39
39
|
const FAMILY_RE = /(?:семь\p{L}*|family|oila(?:ga|lar|li)?|oyla(?:ga|lar|li)?|oila\s+uchun|оилага|оелага|оилавий|oelaga|famil(?:ie|ia)|cuplu(?:ri)?|отбасына|отбасылы|жанұяға|сім['’ʼ]?[яїі](?:ям|ям[иі])?|сімейн\p{L}*)/iu;
|
|
40
|
+
// A category word in an exclusion clause is negative evidence, not a tenant
|
|
41
|
+
// preference. Keep this narrow and local to avoid rejecting ordinary
|
|
42
|
+
// "oila uchun" / "для семьи" invitations elsewhere in the listing.
|
|
43
|
+
const FAMILY_EXCLUSION_RE = /(?:oila|oyla|семь\p{L}*|family|famil(?:ie|ia)|cuplu(?:ri)?|отбас\p{L}*|жанұя\p{L}*)[^\r\n.!?]{0,48}(?:bezovta\s+qilmasin|murojaat\s+qilmasin|kerak\s+emas|qabul\s+qilinmaydi|не\s+(?:беспокоить|принимаем)|not\s+(?:allowed|welcome))/iu;
|
|
40
44
|
const ROOM_SHARE_RE = /(?:sherik(?:ka|lik|likga)?|шерик(?:ка|лик)?|roommate|flatmate|подселени|койко[-\s]?мест|место\s+в\s+(?:комнат|квартир)|birga\s+yashash(?:ga)?|kvartira(?:ga|da)?[^\r\n.!?]{0,36}(?:\d+|bitta|1)\s*(?:ta\s*)?(?:qiz|ayol)[^\r\n.!?]{0,20}(?:ijarachi\s*)?(?:kerak|kere)|coleg\s+de\s+(?:apartament|camer[ăa])|bed\s+space|бөлмелес(?:\s+керек)?|көрші\s+керек)/iu;
|
|
41
45
|
const AIR_CONDITIONER_RE = /(?:кондицион|air\s*con|konditsioner|kandit(?:s|c)?aner|kanditsaner|кандитсанер)/iu;
|
|
42
46
|
const PER_PERSON_PRICE_RE = /(?:kishi\s+boshiga|киши\s+бошига)\s*(\d{1,3}(?:[\s.,]\d{3})*|\d+(?:[.,]\d+)?)\s*(ming|минг|million|mln|млн)?(?:dan|дан)?/iu;
|
|
@@ -176,7 +180,7 @@ export function parseHousingNearby(value) {
|
|
|
176
180
|
|
|
177
181
|
export function parseHousingAudience(value) {
|
|
178
182
|
const text = normalizeUnicode(value ?? '');
|
|
179
|
-
const family = FAMILY_RE.test(text);
|
|
183
|
+
const family = FAMILY_RE.test(text) && !FAMILY_EXCLUSION_RE.test(text);
|
|
180
184
|
const women = WOMEN_RE.test(text);
|
|
181
185
|
const men = MEN_RE.test(text);
|
|
182
186
|
const students = STUDENT_RE.test(text);
|
package/src/housing-money.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type HousingMoneyParseContext = Readonly<{
|
|
|
10
10
|
fallbackCurrency?: string;
|
|
11
11
|
dealType?: 'sale' | 'longRent' | 'shortRent' | string | null;
|
|
12
12
|
}>;
|
|
13
|
-
export type HousingMoneyCandidate = Readonly<{ amount: number; currency: string; start: number; end: number; explicitCurrency: boolean; scale: string | null; priceKeyword: boolean; paymentRole: string; approximate: boolean; confidenceBoost: number; confidence: number }>;
|
|
13
|
+
export type HousingMoneyCandidate = Readonly<{ amount: number; currency: string; start: number; end: number; explicitCurrency: boolean; scale: string | null; priceKeyword: boolean; range: Readonly<{ minimum: number; maximum: number }> | null; paymentRole: string; approximate: boolean; confidenceBoost: number; confidence: number }>;
|
|
14
14
|
export function extractHousingMoneyCandidates(value: unknown, context?: string | HousingMoneyParseContext): readonly HousingMoneyCandidate[];
|
|
15
15
|
export function rankHousingPriceCandidates(candidates: readonly HousingMoneyCandidate[]): readonly HousingMoneyCandidate[];
|
|
16
16
|
|
package/src/housing-money.js
CHANGED
|
@@ -199,7 +199,7 @@ export function extractHousingMoneyCandidates(value, context = '') {
|
|
|
199
199
|
const text = maskPhoneLikeSpans(String(value || ''), ' ', { country });
|
|
200
200
|
const candidates = [];
|
|
201
201
|
const seen = new Set();
|
|
202
|
-
const addCandidate = ({ amount, currency, start, end, explicitCurrency, scale = null, priceKeyword = false, confidenceBoost = 0 }) => {
|
|
202
|
+
const addCandidate = ({ amount, currency, start, end, explicitCurrency, scale = null, priceKeyword = false, range = null, confidenceBoost = 0 }) => {
|
|
203
203
|
if (amount == null || amount < 1 || amount > 5_000_000_000 || seen.has(`${start}:${end}`)) return;
|
|
204
204
|
seen.add(`${start}:${end}`);
|
|
205
205
|
candidates.push(Object.freeze({
|
|
@@ -210,6 +210,7 @@ export function extractHousingMoneyCandidates(value, context = '') {
|
|
|
210
210
|
explicitCurrency,
|
|
211
211
|
scale,
|
|
212
212
|
priceKeyword,
|
|
213
|
+
range,
|
|
213
214
|
paymentRole: candidatePaymentRole(text, start, end),
|
|
214
215
|
approximate: APPROXIMATE_RE.test(text.slice(Math.max(0, start - 12), end)),
|
|
215
216
|
confidenceBoost,
|
|
@@ -239,6 +240,33 @@ export function extractHousingMoneyCandidates(value, context = '') {
|
|
|
239
240
|
});
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
// A price-labelled range is common in Telegram rentals even when the author
|
|
244
|
+
// omits "sum". In Uzbek country context, grouped endpoints are an explicit
|
|
245
|
+
// UZS signal; preserve both endpoints on the candidate while exposing the
|
|
246
|
+
// lower bound through the legacy single-price result. It must be collected
|
|
247
|
+
// before generic amounts so the second endpoint cannot be selected merely
|
|
248
|
+
// because it is larger.
|
|
249
|
+
const labelledPriceRangeRe = new RegExp(
|
|
250
|
+
`${PRICE_KEYWORD}\\s*[:=\\-–—]?\\s*(${MONEY_NUMBER_PATTERN})\\s*(?:-{1,3}|–|—|to|до|dan\\s+gacha)\\s*(${MONEY_NUMBER_PATTERN})(?=$|[^\\p{L}\\p{N}_])`,
|
|
251
|
+
'igu',
|
|
252
|
+
);
|
|
253
|
+
for (const match of text.matchAll(labelledPriceRangeRe)) {
|
|
254
|
+
const minimum = parseNumericAmount(match[1]);
|
|
255
|
+
const maximum = parseNumericAmount(match[2]);
|
|
256
|
+
if (minimum == null || maximum == null || minimum > maximum) continue;
|
|
257
|
+
const start = match.index ?? 0;
|
|
258
|
+
addCandidate({
|
|
259
|
+
amount: Math.round(minimum),
|
|
260
|
+
currency: country === 'UZ' ? 'UZS' : fallbackCurrency || '',
|
|
261
|
+
start,
|
|
262
|
+
end: start + match[0].length,
|
|
263
|
+
explicitCurrency: false,
|
|
264
|
+
priceKeyword: true,
|
|
265
|
+
range: Object.freeze({ minimum: Math.round(minimum), maximum: Math.round(maximum) }),
|
|
266
|
+
confidenceBoost: 0.2,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
242
270
|
const splitMillionRe = /(?:^|[^\p{L}\p{N}_])(\d{1,3})\s*(?:млн\.?|mln\.?|миллион(?:а|ов)?|million(?:s)?)\s+(\d{1,3})(?=$|[^\p{L}\p{N}_])/giu;
|
|
243
271
|
for (const match of text.matchAll(splitMillionRe)) {
|
|
244
272
|
const start = match.index ?? 0;
|
|
@@ -87,7 +87,7 @@ export function parseHousingRoomCount(value) {
|
|
|
87
87
|
if (total >= 1 && total <= 20) return total;
|
|
88
88
|
}
|
|
89
89
|
for (const [re, rooms] of NUMBER_WORDS) if (re.test(text)) return rooms;
|
|
90
|
-
const numeric = text.match(/(?:^|[^\p{L}\p{N}])(\d{1,2})\s*(?:(?:-\s*)?комнат\p{L}*|(?:-\s*)?к(?:\.|\b)|(?:-\s*)?xona(?:li)?|(?:-\s*)?хона(
|
|
90
|
+
const numeric = text.match(/(?:^|[^\p{L}\p{N}])(\d{1,2})\s*(?:ta\s*)?(?:(?:-\s*)?комнат\p{L}*|(?:-\s*)?к(?:\.|\b)|(?:-\s*)?xona(?:li|si|lari)?|(?:-\s*)?хона(?:лик|ли|си|лари)?|бөлмелі|rooms?)(?=$|[^\p{L}\p{N}])/iu);
|
|
91
91
|
if (numeric) {
|
|
92
92
|
const rooms = toNumber(numeric[1]);
|
|
93
93
|
if (rooms != null && rooms >= 1 && rooms <= 20) return rooms;
|