@whiteslove/parsing-lexicon 0.2.4 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for Whiteslove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
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
+
@@ -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
+
@@ -130,3 +130,8 @@ export function resolveHousingIntent(value) {
130
130
  dealType: durationDeal.canonical,
131
131
  });
132
132
  }
133
+
134
+ export function classifyHousingDealType(value) {
135
+ return resolveHousingIntent(value)?.dealType || null;
136
+ }
137
+
@@ -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 commissionPercent = toNumber(text.match(/(?:комисси\p{L}*|commission|comision|komissiya)[^\d%]{0,16}(\d{1,3}(?:[.,]\d+)?)\s*%/iu)?.[1]);
131
- const commissionMentioned = SELLER_TERMS.commission && findCanonical(text, [SELLER_TERMS.commission], { partial: true });
130
+ const shorthandCommission = text.match(/(?:^|[^\p{L}\p{N}_])[]\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 });
@@ -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
+
@@ -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: ['uy', 'hovli', 'xovli'], uzCyrl: ['уй', 'ҳовли', 'ховли'], kk: ['үй', 'жеке үй', 'коттедж'],
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
+