@whiteslove/parsing-lexicon 0.2.0 → 0.2.2
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/LICENSE +21 -21
- package/README.md +137 -137
- package/index.d.ts +374 -374
- package/package.json +77 -75
- package/src/central-asia-locations.js +211 -211
- package/src/central-asia.js +233 -233
- package/src/contact.d.ts +44 -44
- package/src/contact.js +159 -159
- package/src/countries.js +175 -175
- package/src/country-context.d.ts +10 -10
- package/src/country-context.js +22 -22
- package/src/currency.d.ts +14 -14
- package/src/currency.js +66 -66
- package/src/geo.js +169 -169
- package/src/geography-central-asia.js +42 -42
- package/src/geography-detection.d.ts +14 -14
- package/src/geography-detection.js +64 -64
- package/src/geography-display.d.ts +13 -13
- package/src/geography-display.js +113 -113
- package/src/geography.js +210 -210
- package/src/hiring-advanced.js +147 -147
- package/src/hiring-candidate-fields.d.ts +9 -9
- package/src/hiring-candidate-fields.js +92 -92
- package/src/hiring-context.d.ts +64 -64
- package/src/hiring-context.js +412 -412
- package/src/hiring-language-extensions.d.ts +19 -19
- package/src/hiring-language-extensions.js +139 -139
- package/src/hiring-languages.js +135 -135
- package/src/hiring-location-fields.d.ts +3 -3
- package/src/hiring-location-fields.js +53 -53
- package/src/hiring-professions-ro.js +191 -191
- package/src/hiring-professions.js +437 -437
- package/src/hiring-requirements.d.ts +16 -16
- package/src/hiring-requirements.js +170 -170
- package/src/hiring-salary-context.d.ts +27 -27
- package/src/hiring-salary-context.js +76 -76
- package/src/hiring-semantics.d.ts +29 -29
- package/src/hiring-semantics.js +218 -218
- package/src/hiring-skills.d.ts +21 -21
- package/src/hiring-skills.js +315 -315
- package/src/hiring-source-aliases.d.ts +32 -32
- package/src/hiring-source-aliases.js +185 -185
- package/src/hiring-source-semantics.d.ts +31 -31
- package/src/hiring-source-semantics.js +293 -293
- package/src/hiring-temporal.d.ts +15 -15
- package/src/hiring-temporal.js +67 -67
- package/src/hiring-vacancy-fields.d.ts +5 -5
- package/src/hiring-vacancy-fields.js +23 -23
- package/src/hiring-work-semantics.d.ts +34 -34
- package/src/hiring-work-semantics.js +72 -72
- package/src/hiring.js +180 -180
- package/src/housing-address.d.ts +18 -18
- package/src/housing-address.js +217 -210
- package/src/housing-commercial.d.ts +1 -0
- package/src/housing-commercial.js +9 -0
- package/src/housing-context.d.ts +36 -36
- package/src/housing-context.js +183 -183
- package/src/housing-features.js +30 -30
- package/src/housing-intent.d.ts +9 -9
- package/src/housing-intent.js +132 -132
- package/src/housing-listing-fields.js +131 -131
- package/src/housing-money.d.ts +7 -7
- package/src/housing-money.js +102 -102
- package/src/housing-source-aliases.d.ts +8 -8
- package/src/housing-source-aliases.js +51 -51
- package/src/housing-structured.d.ts +55 -55
- package/src/housing-structured.js +219 -219
- package/src/housing.js +200 -200
- package/src/index.js +45 -44
- package/src/kz-location-extensions.js +307 -307
- package/src/landmarks.js +21 -21
- package/src/lexicon-core.js +58 -58
- package/src/location-merge.js +106 -106
- package/src/locations.js +456 -456
- package/src/money-core.d.ts +10 -10
- package/src/money-core.js +81 -81
- package/src/money-lexicon.d.ts +5 -5
- package/src/money-lexicon.js +79 -79
- package/src/money.js +154 -154
- package/src/normalization.js +384 -384
- package/src/odesa-metropolitan.js +144 -144
- package/src/romania-geography.js +47 -47
- package/src/tashkent-colloquial.js +80 -72
- package/src/tashkent-housing-geography.d.ts +38 -38
- package/src/tashkent-housing-geography.js +211 -211
- package/src/tashkent-pois.js +215 -215
- package/src/tashkent-residential-complexes.js +272 -272
- package/src/ua-location-extensions-major.js +118 -118
- package/src/ua-location-extensions-metro.js +9 -9
- package/src/ua-location-extensions-regional.js +246 -246
- package/src/ua-secondary-cities.d.ts +2 -2
- package/src/ua-secondary-cities.js +57 -57
- package/src/ukraine.js +75 -75
- package/src/uz-location-extensions.js +259 -259
package/src/housing-money.js
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
import { aliasesOf, findCanonical } from './normalization.js';
|
|
2
|
-
import { CURRENCY_TERMS, NUMBER_MULTIPLIERS } from './money-lexicon.js';
|
|
3
|
-
import {
|
|
4
|
-
MONEY_NUMBER_PATTERN,
|
|
5
|
-
moneyCurrencyFromText,
|
|
6
|
-
moneyCurrencyPattern,
|
|
7
|
-
parseNumericAmount,
|
|
8
|
-
parseScaledAmount,
|
|
9
|
-
} from './money-core.js';
|
|
10
|
-
import { maskPhoneLikeSpans } from './contact.js';
|
|
11
|
-
|
|
12
|
-
const PRICE_KEYWORD = '(?:цена|ціна|нарх(?:и)?|narx|price|стоимост[ьи]|аренд(?:а|ная\\s+плата)?|rent)';
|
|
13
|
-
const PRICE_CURRENCY = `(?:${moneyCurrencyPattern()})`;
|
|
14
|
-
|
|
15
|
-
function escapeRegex(value) {
|
|
16
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function parseHousingPrice(value, fallbackCurrency = '') {
|
|
20
|
-
const original = String(value || '');
|
|
21
|
-
if (!original) return Object.freeze({ price: null, currency: fallbackCurrency || '' });
|
|
22
|
-
|
|
23
|
-
// Contact spans are removed once, before every money branch. A phone can
|
|
24
|
-
// therefore never win as a labelled, currency-tagged or fallback amount.
|
|
25
|
-
const text = maskPhoneLikeSpans(original);
|
|
26
|
-
let currency = moneyCurrencyFromText(text, fallbackCurrency || '') || '';
|
|
27
|
-
const explicit = Boolean(findCanonical(text, CURRENCY_TERMS, { partial: true }));
|
|
28
|
-
let price = null;
|
|
29
|
-
|
|
30
|
-
// Common Ukrainian/Russian classifieds shorthand: "10 т грн" / "10 т гр".
|
|
31
|
-
// Keep this housing-specific instead of adding globally ambiguous aliases
|
|
32
|
-
// `т` (tonne) and `гр` (gram) to the shared money lexicon. A nearby price
|
|
33
|
-
// keyword plus an explicit hryvnia shorthand makes the intent unambiguous.
|
|
34
|
-
const compactThousandUah = text.match(new RegExp(
|
|
35
|
-
`${PRICE_KEYWORD}[^\\r\\n]{0,48}?(\\d{1,6}(?:[.,]\\d{1,2})?)\\s*т(?:ыс\\.?)?\\s*(?:гр(?:н)?|₴|uah)(?=$|[^\\p{L}\\p{N}_])`,
|
|
36
|
-
'iu',
|
|
37
|
-
));
|
|
38
|
-
if (compactThousandUah) {
|
|
39
|
-
const amount = parseNumericAmount(compactThousandUah[1]);
|
|
40
|
-
if (amount != null && amount >= 1 && amount <= 5_000_000) {
|
|
41
|
-
price = Math.round(amount * 1000);
|
|
42
|
-
currency = 'UAH';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const labelled = text.match(new RegExp(`${PRICE_KEYWORD}\\s*[:\\-–—]?\\s*(${MONEY_NUMBER_PATTERN})`, 'i'));
|
|
47
|
-
if (price == null && labelled) {
|
|
48
|
-
const amount = parseNumericAmount(labelled[1]);
|
|
49
|
-
if (amount != null && amount >= 50 && amount <= 5_000_000_000) price = amount;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (price == null) {
|
|
53
|
-
let tagged = null;
|
|
54
|
-
const reNumSym = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*${PRICE_CURRENCY}`, 'ig');
|
|
55
|
-
const reSymNum = new RegExp(`${PRICE_CURRENCY}\\s*(${MONEY_NUMBER_PATTERN})`, 'ig');
|
|
56
|
-
for (const regex of [reNumSym, reSymNum]) {
|
|
57
|
-
let match;
|
|
58
|
-
while ((match = regex.exec(text)) !== null) {
|
|
59
|
-
const amount = parseNumericAmount(match[1]);
|
|
60
|
-
if (amount != null && amount >= 50 && amount <= 5_000_000_000 && (tagged == null || amount > tagged)) tagged = amount;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
price = tagged;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (price == null) {
|
|
67
|
-
const scalePattern = [...new Set(NUMBER_MULTIPLIERS.flatMap((entry) => aliasesOf(entry)).filter(Boolean))]
|
|
68
|
-
.sort((a, b) => String(b).length - String(a).length)
|
|
69
|
-
.map(escapeRegex)
|
|
70
|
-
.join('|');
|
|
71
|
-
// Multiplier aliases include useful one-letter forms such as `m` and `k`.
|
|
72
|
-
// Require the alias to end at a token boundary so measurements/words like
|
|
73
|
-
// `500 m2` and `5 minut` cannot be promoted to 500 million / 5 million.
|
|
74
|
-
const match = text.match(new RegExp(`(\\d+(?:[.,]\\d+)?)\\s*(${scalePattern})(?=$|[^\\p{L}\\p{N}_])`, 'iu'));
|
|
75
|
-
if (match) {
|
|
76
|
-
const amount = parseScaledAmount(match[1], match[2]);
|
|
77
|
-
if (amount != null && amount >= 1000 && amount <= 5_000_000_000) price = Math.round(amount);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (price == null) {
|
|
82
|
-
const matches = text.match(/\d{1,3}(?:[ \u00A0.,]\d{3})+|\d{4,}/g) || [];
|
|
83
|
-
let best = null;
|
|
84
|
-
for (const raw of matches) {
|
|
85
|
-
const digits = raw.replace(/[\s.,]/g, '');
|
|
86
|
-
if (digits[0] === '0') continue;
|
|
87
|
-
const amount = parseNumericAmount(raw);
|
|
88
|
-
if (amount != null && amount >= 1000 && amount <= 5_000_000_000 && (best == null || amount > best)) best = amount;
|
|
89
|
-
}
|
|
90
|
-
price = best;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!explicit && fallbackCurrency === 'UZS' && price != null) {
|
|
94
|
-
const dailyUzbek = /(?:kunlik|sutkaga|kecha[- ]?kunduz|посуточн|суточн)/i.test(text);
|
|
95
|
-
currency = price >= 1_000_000 || (dailyUzbek && price >= 10_000) ? 'UZS' : 'USD';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return Object.freeze({ price, currency });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Compatibility name for callers migrating from Flat Finder's local parser.
|
|
102
|
-
export const parsePriceFromText = parseHousingPrice;
|
|
1
|
+
import { aliasesOf, findCanonical } from './normalization.js';
|
|
2
|
+
import { CURRENCY_TERMS, NUMBER_MULTIPLIERS } from './money-lexicon.js';
|
|
3
|
+
import {
|
|
4
|
+
MONEY_NUMBER_PATTERN,
|
|
5
|
+
moneyCurrencyFromText,
|
|
6
|
+
moneyCurrencyPattern,
|
|
7
|
+
parseNumericAmount,
|
|
8
|
+
parseScaledAmount,
|
|
9
|
+
} from './money-core.js';
|
|
10
|
+
import { maskPhoneLikeSpans } from './contact.js';
|
|
11
|
+
|
|
12
|
+
const PRICE_KEYWORD = '(?:цена|ціна|нарх(?:и)?|narx|price|стоимост[ьи]|аренд(?:а|ная\\s+плата)?|rent)';
|
|
13
|
+
const PRICE_CURRENCY = `(?:${moneyCurrencyPattern()})`;
|
|
14
|
+
|
|
15
|
+
function escapeRegex(value) {
|
|
16
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseHousingPrice(value, fallbackCurrency = '') {
|
|
20
|
+
const original = String(value || '');
|
|
21
|
+
if (!original) return Object.freeze({ price: null, currency: fallbackCurrency || '' });
|
|
22
|
+
|
|
23
|
+
// Contact spans are removed once, before every money branch. A phone can
|
|
24
|
+
// therefore never win as a labelled, currency-tagged or fallback amount.
|
|
25
|
+
const text = maskPhoneLikeSpans(original);
|
|
26
|
+
let currency = moneyCurrencyFromText(text, fallbackCurrency || '') || '';
|
|
27
|
+
const explicit = Boolean(findCanonical(text, CURRENCY_TERMS, { partial: true }));
|
|
28
|
+
let price = null;
|
|
29
|
+
|
|
30
|
+
// Common Ukrainian/Russian classifieds shorthand: "10 т грн" / "10 т гр".
|
|
31
|
+
// Keep this housing-specific instead of adding globally ambiguous aliases
|
|
32
|
+
// `т` (tonne) and `гр` (gram) to the shared money lexicon. A nearby price
|
|
33
|
+
// keyword plus an explicit hryvnia shorthand makes the intent unambiguous.
|
|
34
|
+
const compactThousandUah = text.match(new RegExp(
|
|
35
|
+
`${PRICE_KEYWORD}[^\\r\\n]{0,48}?(\\d{1,6}(?:[.,]\\d{1,2})?)\\s*т(?:ыс\\.?)?\\s*(?:гр(?:н)?|₴|uah)(?=$|[^\\p{L}\\p{N}_])`,
|
|
36
|
+
'iu',
|
|
37
|
+
));
|
|
38
|
+
if (compactThousandUah) {
|
|
39
|
+
const amount = parseNumericAmount(compactThousandUah[1]);
|
|
40
|
+
if (amount != null && amount >= 1 && amount <= 5_000_000) {
|
|
41
|
+
price = Math.round(amount * 1000);
|
|
42
|
+
currency = 'UAH';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const labelled = text.match(new RegExp(`${PRICE_KEYWORD}\\s*[:\\-–—]?\\s*(${MONEY_NUMBER_PATTERN})`, 'i'));
|
|
47
|
+
if (price == null && labelled) {
|
|
48
|
+
const amount = parseNumericAmount(labelled[1]);
|
|
49
|
+
if (amount != null && amount >= 50 && amount <= 5_000_000_000) price = amount;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (price == null) {
|
|
53
|
+
let tagged = null;
|
|
54
|
+
const reNumSym = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*${PRICE_CURRENCY}`, 'ig');
|
|
55
|
+
const reSymNum = new RegExp(`${PRICE_CURRENCY}\\s*(${MONEY_NUMBER_PATTERN})`, 'ig');
|
|
56
|
+
for (const regex of [reNumSym, reSymNum]) {
|
|
57
|
+
let match;
|
|
58
|
+
while ((match = regex.exec(text)) !== null) {
|
|
59
|
+
const amount = parseNumericAmount(match[1]);
|
|
60
|
+
if (amount != null && amount >= 50 && amount <= 5_000_000_000 && (tagged == null || amount > tagged)) tagged = amount;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
price = tagged;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (price == null) {
|
|
67
|
+
const scalePattern = [...new Set(NUMBER_MULTIPLIERS.flatMap((entry) => aliasesOf(entry)).filter(Boolean))]
|
|
68
|
+
.sort((a, b) => String(b).length - String(a).length)
|
|
69
|
+
.map(escapeRegex)
|
|
70
|
+
.join('|');
|
|
71
|
+
// Multiplier aliases include useful one-letter forms such as `m` and `k`.
|
|
72
|
+
// Require the alias to end at a token boundary so measurements/words like
|
|
73
|
+
// `500 m2` and `5 minut` cannot be promoted to 500 million / 5 million.
|
|
74
|
+
const match = text.match(new RegExp(`(\\d+(?:[.,]\\d+)?)\\s*(${scalePattern})(?=$|[^\\p{L}\\p{N}_])`, 'iu'));
|
|
75
|
+
if (match) {
|
|
76
|
+
const amount = parseScaledAmount(match[1], match[2]);
|
|
77
|
+
if (amount != null && amount >= 1000 && amount <= 5_000_000_000) price = Math.round(amount);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (price == null) {
|
|
82
|
+
const matches = text.match(/\d{1,3}(?:[ \u00A0.,]\d{3})+|\d{4,}/g) || [];
|
|
83
|
+
let best = null;
|
|
84
|
+
for (const raw of matches) {
|
|
85
|
+
const digits = raw.replace(/[\s.,]/g, '');
|
|
86
|
+
if (digits[0] === '0') continue;
|
|
87
|
+
const amount = parseNumericAmount(raw);
|
|
88
|
+
if (amount != null && amount >= 1000 && amount <= 5_000_000_000 && (best == null || amount > best)) best = amount;
|
|
89
|
+
}
|
|
90
|
+
price = best;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!explicit && fallbackCurrency === 'UZS' && price != null) {
|
|
94
|
+
const dailyUzbek = /(?:kunlik|sutkaga|kecha[- ]?kunduz|посуточн|суточн)/i.test(text);
|
|
95
|
+
currency = price >= 1_000_000 || (dailyUzbek && price >= 10_000) ? 'UZS' : 'USD';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Object.freeze({ price, currency });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Compatibility name for callers migrating from Flat Finder's local parser.
|
|
102
|
+
export const parsePriceFromText = parseHousingPrice;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const HOUSING_DEAL_TYPE_EXTENSIONS: readonly unknown[];
|
|
2
|
-
export const HOUSING_ROOM_ONLY_EXTENSIONS: readonly unknown[];
|
|
3
|
-
export function resolveExtendedHousingIntent(value: unknown): Readonly<{
|
|
4
|
-
action: string | null;
|
|
5
|
-
listingKind: string | null;
|
|
6
|
-
dealType: 'sale' | 'longRent' | 'shortRent';
|
|
7
|
-
}> | null;
|
|
8
|
-
export function isRoomOnlyHousing(value: unknown): boolean;
|
|
1
|
+
export const HOUSING_DEAL_TYPE_EXTENSIONS: readonly unknown[];
|
|
2
|
+
export const HOUSING_ROOM_ONLY_EXTENSIONS: readonly unknown[];
|
|
3
|
+
export function resolveExtendedHousingIntent(value: unknown): Readonly<{
|
|
4
|
+
action: string | null;
|
|
5
|
+
listingKind: string | null;
|
|
6
|
+
dealType: 'sale' | 'longRent' | 'shortRent';
|
|
7
|
+
}> | null;
|
|
8
|
+
export function isRoomOnlyHousing(value: unknown): boolean;
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { lexiconEntity } from './lexicon-core.js';
|
|
2
|
-
import { findCanonical } from './normalization.js';
|
|
3
|
-
import { resolveHousingIntent } from './housing-intent.js';
|
|
4
|
-
import { HOUSING_OCCUPANCY_TYPES, PROPERTY_TYPES } from './housing.js';
|
|
5
|
-
|
|
6
|
-
const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
|
|
7
|
-
|
|
8
|
-
/** Source/feed wording that is useful for normalization but too noisy for the generic core. */
|
|
9
|
-
export const HOUSING_DEAL_TYPE_EXTENSIONS = Object.freeze([
|
|
10
|
-
group('longRent', {
|
|
11
|
-
ru: ['снять', 'в месяц', 'месяц'],
|
|
12
|
-
en: ['monthly'],
|
|
13
|
-
uzLatn: ['ijaraga', 'oyiga', 'beriladi'],
|
|
14
|
-
uzCyrl: ['ижарага', 'ойига', 'берилади'],
|
|
15
|
-
kk: ['айына'],
|
|
16
|
-
}),
|
|
17
|
-
group('shortRent', {
|
|
18
|
-
ru: ['сутки', 'суток', 'суточно'],
|
|
19
|
-
en: ['daily'],
|
|
20
|
-
uzLatn: ['kunlik', 'sutkalik'],
|
|
21
|
-
uzCyrl: ['кунлик', 'суткалик'],
|
|
22
|
-
kk: ['тәуліктік'],
|
|
23
|
-
}),
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
export const HOUSING_ROOM_ONLY_EXTENSIONS = Object.freeze([
|
|
27
|
-
group('roomOnly', {
|
|
28
|
-
ru: ['шеринг'],
|
|
29
|
-
en: ['room only'],
|
|
30
|
-
uzLatn: ['student qizlarga', 'talaba qizlarga', 'opshijit dom', 'obshijit dom'],
|
|
31
|
-
uzCyrl: ['студент қизларга', 'талаба қизларга'],
|
|
32
|
-
}),
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
export function resolveExtendedHousingIntent(value) {
|
|
36
|
-
const text = String(value || '');
|
|
37
|
-
const base = resolveHousingIntent(text);
|
|
38
|
-
if (base) return base;
|
|
39
|
-
const deal = findCanonical(text, HOUSING_DEAL_TYPE_EXTENSIONS, { partial: true });
|
|
40
|
-
if (!deal?.canonical) return null;
|
|
41
|
-
return Object.freeze({ action: null, listingKind: null, dealType: deal.canonical });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function isRoomOnlyHousing(value) {
|
|
45
|
-
const text = String(value || '');
|
|
46
|
-
if (!text.trim()) return false;
|
|
47
|
-
const occupancy = findCanonical(text, HOUSING_OCCUPANCY_TYPES, { partial: true })?.canonical;
|
|
48
|
-
if (occupancy && ['room', 'sharedRoom', 'bedSpace'].includes(occupancy)) return true;
|
|
49
|
-
if (findCanonical(text, PROPERTY_TYPES, { partial: true })?.canonical === 'dormitory') return true;
|
|
50
|
-
return Boolean(findCanonical(text, HOUSING_ROOM_ONLY_EXTENSIONS, { partial: true }));
|
|
51
|
-
}
|
|
1
|
+
import { lexiconEntity } from './lexicon-core.js';
|
|
2
|
+
import { findCanonical } from './normalization.js';
|
|
3
|
+
import { resolveHousingIntent } from './housing-intent.js';
|
|
4
|
+
import { HOUSING_OCCUPANCY_TYPES, PROPERTY_TYPES } from './housing.js';
|
|
5
|
+
|
|
6
|
+
const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
|
|
7
|
+
|
|
8
|
+
/** Source/feed wording that is useful for normalization but too noisy for the generic core. */
|
|
9
|
+
export const HOUSING_DEAL_TYPE_EXTENSIONS = Object.freeze([
|
|
10
|
+
group('longRent', {
|
|
11
|
+
ru: ['снять', 'в месяц', 'месяц'],
|
|
12
|
+
en: ['monthly'],
|
|
13
|
+
uzLatn: ['ijaraga', 'oyiga', 'beriladi'],
|
|
14
|
+
uzCyrl: ['ижарага', 'ойига', 'берилади'],
|
|
15
|
+
kk: ['айына'],
|
|
16
|
+
}),
|
|
17
|
+
group('shortRent', {
|
|
18
|
+
ru: ['сутки', 'суток', 'суточно'],
|
|
19
|
+
en: ['daily'],
|
|
20
|
+
uzLatn: ['kunlik', 'sutkalik'],
|
|
21
|
+
uzCyrl: ['кунлик', 'суткалик'],
|
|
22
|
+
kk: ['тәуліктік'],
|
|
23
|
+
}),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export const HOUSING_ROOM_ONLY_EXTENSIONS = Object.freeze([
|
|
27
|
+
group('roomOnly', {
|
|
28
|
+
ru: ['шеринг'],
|
|
29
|
+
en: ['room only'],
|
|
30
|
+
uzLatn: ['student qizlarga', 'talaba qizlarga', 'opshijit dom', 'obshijit dom'],
|
|
31
|
+
uzCyrl: ['студент қизларга', 'талаба қизларга'],
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function resolveExtendedHousingIntent(value) {
|
|
36
|
+
const text = String(value || '');
|
|
37
|
+
const base = resolveHousingIntent(text);
|
|
38
|
+
if (base) return base;
|
|
39
|
+
const deal = findCanonical(text, HOUSING_DEAL_TYPE_EXTENSIONS, { partial: true });
|
|
40
|
+
if (!deal?.canonical) return null;
|
|
41
|
+
return Object.freeze({ action: null, listingKind: null, dealType: deal.canonical });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isRoomOnlyHousing(value) {
|
|
45
|
+
const text = String(value || '');
|
|
46
|
+
if (!text.trim()) return false;
|
|
47
|
+
const occupancy = findCanonical(text, HOUSING_OCCUPANCY_TYPES, { partial: true })?.canonical;
|
|
48
|
+
if (occupancy && ['room', 'sharedRoom', 'bedSpace'].includes(occupancy)) return true;
|
|
49
|
+
if (findCanonical(text, PROPERTY_TYPES, { partial: true })?.canonical === 'dormitory') return true;
|
|
50
|
+
return Boolean(findCanonical(text, HOUSING_ROOM_ONLY_EXTENSIONS, { partial: true }));
|
|
51
|
+
}
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
export type HousingAreaDetails = Readonly<{
|
|
2
|
-
total: number | null;
|
|
3
|
-
living: number | null;
|
|
4
|
-
kitchen: number | null;
|
|
5
|
-
balcony: number | null;
|
|
6
|
-
terrace: number | null;
|
|
7
|
-
}>;
|
|
8
|
-
|
|
9
|
-
export type HousingPaymentContext = Readonly<{
|
|
10
|
-
deposit: Readonly<{
|
|
11
|
-
required: boolean | null;
|
|
12
|
-
kind: string | null;
|
|
13
|
-
amount: number | null;
|
|
14
|
-
currency: string | null;
|
|
15
|
-
}>;
|
|
16
|
-
prepaymentMonths: number | null;
|
|
17
|
-
utilities: string | null;
|
|
18
|
-
commission: Readonly<{
|
|
19
|
-
required: boolean | null;
|
|
20
|
-
percent: number | null;
|
|
21
|
-
}>;
|
|
22
|
-
}>;
|
|
23
|
-
|
|
24
|
-
export type HousingInfrastructureDistance = Readonly<{
|
|
25
|
-
value: number | null;
|
|
26
|
-
unit: 'minute' | 'meter' | 'kilometer';
|
|
27
|
-
mode: 'walk' | 'drive' | null;
|
|
28
|
-
}>;
|
|
29
|
-
|
|
30
|
-
export type HousingInfrastructureMatch = Readonly<{
|
|
31
|
-
poi: string;
|
|
32
|
-
relation: string | null;
|
|
33
|
-
distance: HousingInfrastructureDistance | null;
|
|
34
|
-
start: number;
|
|
35
|
-
end: number;
|
|
36
|
-
}>;
|
|
37
|
-
|
|
38
|
-
export type HousingStructuredResult = Readonly<{
|
|
39
|
-
intent: Readonly<{ action: string | null; listingKind: string | null; dealType: string }> | null;
|
|
40
|
-
context: Readonly<Record<string, unknown>>;
|
|
41
|
-
rooms: number | null;
|
|
42
|
-
floor: Readonly<{ floor: number | null; totalFloors: number | null }>;
|
|
43
|
-
area: HousingAreaDetails;
|
|
44
|
-
payments: HousingPaymentContext;
|
|
45
|
-
seller: Readonly<{ type: 'owner' | 'agency' | null; confidence: number }>;
|
|
46
|
-
infrastructure: readonly HousingInfrastructureMatch[];
|
|
47
|
-
}>;
|
|
48
|
-
|
|
49
|
-
export function parseHousingRoomCount(value: unknown): number | null;
|
|
50
|
-
export function parseHousingFloor(value: unknown): Readonly<{ floor: number | null; totalFloors: number | null }>;
|
|
51
|
-
export function parseHousingAreas(value: unknown): HousingAreaDetails;
|
|
52
|
-
export function parseHousingPayments(value: unknown): HousingPaymentContext;
|
|
53
|
-
export function parseHousingSeller(value: unknown): Readonly<{ type: 'owner' | 'agency' | null; confidence: number }>;
|
|
54
|
-
export function parseHousingInfrastructure(value: unknown): readonly HousingInfrastructureMatch[];
|
|
55
|
-
export function parseHousingStructured(value: unknown): HousingStructuredResult;
|
|
1
|
+
export type HousingAreaDetails = Readonly<{
|
|
2
|
+
total: number | null;
|
|
3
|
+
living: number | null;
|
|
4
|
+
kitchen: number | null;
|
|
5
|
+
balcony: number | null;
|
|
6
|
+
terrace: number | null;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
export type HousingPaymentContext = Readonly<{
|
|
10
|
+
deposit: Readonly<{
|
|
11
|
+
required: boolean | null;
|
|
12
|
+
kind: string | null;
|
|
13
|
+
amount: number | null;
|
|
14
|
+
currency: string | null;
|
|
15
|
+
}>;
|
|
16
|
+
prepaymentMonths: number | null;
|
|
17
|
+
utilities: string | null;
|
|
18
|
+
commission: Readonly<{
|
|
19
|
+
required: boolean | null;
|
|
20
|
+
percent: number | null;
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export type HousingInfrastructureDistance = Readonly<{
|
|
25
|
+
value: number | null;
|
|
26
|
+
unit: 'minute' | 'meter' | 'kilometer';
|
|
27
|
+
mode: 'walk' | 'drive' | null;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
export type HousingInfrastructureMatch = Readonly<{
|
|
31
|
+
poi: string;
|
|
32
|
+
relation: string | null;
|
|
33
|
+
distance: HousingInfrastructureDistance | null;
|
|
34
|
+
start: number;
|
|
35
|
+
end: number;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
export type HousingStructuredResult = Readonly<{
|
|
39
|
+
intent: Readonly<{ action: string | null; listingKind: string | null; dealType: string }> | null;
|
|
40
|
+
context: Readonly<Record<string, unknown>>;
|
|
41
|
+
rooms: number | null;
|
|
42
|
+
floor: Readonly<{ floor: number | null; totalFloors: number | null }>;
|
|
43
|
+
area: HousingAreaDetails;
|
|
44
|
+
payments: HousingPaymentContext;
|
|
45
|
+
seller: Readonly<{ type: 'owner' | 'agency' | null; confidence: number }>;
|
|
46
|
+
infrastructure: readonly HousingInfrastructureMatch[];
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
export function parseHousingRoomCount(value: unknown): number | null;
|
|
50
|
+
export function parseHousingFloor(value: unknown): Readonly<{ floor: number | null; totalFloors: number | null }>;
|
|
51
|
+
export function parseHousingAreas(value: unknown): HousingAreaDetails;
|
|
52
|
+
export function parseHousingPayments(value: unknown): HousingPaymentContext;
|
|
53
|
+
export function parseHousingSeller(value: unknown): Readonly<{ type: 'owner' | 'agency' | null; confidence: number }>;
|
|
54
|
+
export function parseHousingInfrastructure(value: unknown): readonly HousingInfrastructureMatch[];
|
|
55
|
+
export function parseHousingStructured(value: unknown): HousingStructuredResult;
|