@whiteslove/parsing-lexicon 0.10.8 → 0.10.9

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -368,8 +368,11 @@ export function canonicalSkillName(value) {
368
368
  return CANONICAL_BY_ALIAS.get(normalized) ?? extractSkillNames(normalized)[0]
369
369
  }
370
370
 
371
+ const PAY_BONUS_KPI_RE = /(?:\+|plus|плюс)\s*(?:%\s*\/?\s*)?kpi(?![\p{L}\p{N}_])/giu
372
+
371
373
  export function extractSkillDetails(text) {
372
- const normalized = normalizeSkillText(cleanHiringSourceText(text))
374
+ // "+ KPI" / "plus KPI" after a pay figure is a bonus scheme, not a skill.
375
+ const normalized = normalizeSkillText(cleanHiringSourceText(text).replace(PAY_BONUS_KPI_RE, ' '))
373
376
  const found = []
374
377
  const names = new Set()
375
378
  for (const { definition, patterns } of COMPILED_SKILLS) {
@@ -76,8 +76,8 @@ export function extractJobStructuredField(value, key, maxLength = 220) {
76
76
  return extractField(value, JOB_FIELD_TERMS[key], FIELD_EXTRA_ALIASES.job[key] || [], maxLength);
77
77
  }
78
78
 
79
- const INLINE_VACANCY_HEADING_RE = /✔️?\s*([A-ZА-ЯЁЎҚҒҲІЇЄ][A-ZА-ЯЁЎҚҒҲІЇЄ\s()\/-]{2,100}?)(?=\s*(?:[•⏳✔📍✈📞]|$))/gu;
80
- const SHARED_HIRING_CONTACT_RE = /(?:✈️?\s*Telegram\s*:|📞\s*Контакт\s*:)/iu;
79
+ const INLINE_VACANCY_HEADING_RE = /(?:✔️?|▶️?)\s*([A-ZА-ЯЁЎҚҒҲІЇЄ][A-ZА-ЯЁЎҚҒҲІЇЄ\s()\/-]{2,100}?(?:\s*\([^)\n]{1,30}\))?)(?=\s*(?:[:•⏳✔▶📍✈📞💰]|$))/gu;
80
+ const SHARED_HIRING_CONTACT_RE = /(?:✈️?\s*Telegram\s*:|📞\s*Контакт\s*:|👍\s*ГРАФИК\s+РАБОТЫ)/iu;
81
81
 
82
82
  /** Split a multi-vacancy source post into role-local blocks and shared context. */
83
83
  export function splitHiringVacancyEntries(value) {
package/src/money-core.js CHANGED
@@ -22,7 +22,10 @@ export const MONEY_SCALE_PATTERN = 'k|к|тыс\\.?|тысяч(?:а|и)?|тис\
22
22
  // present at all) would stop matching, since "до" doesn't satisfy the
23
23
  // boundary either.
24
24
  export const MONEY_RANGE_RE = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*(?:(${MONEY_SCALE_PATTERN})(?=$|[^\\p{L}\\p{N}_]))?\\s*(?:-|–|—|до|to|bis|dan\\s+gacha)\\s*(${MONEY_NUMBER_PATTERN})\\s*(?:(${MONEY_SCALE_PATTERN})(?=$|[^\\p{L}\\p{N}_]))?`, 'iu');
25
- export const MONEY_SINGLE_RE = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*(${MONEY_SCALE_PATTERN})?(?=$|[^\\p{L}\\p{N}_])`, 'giu');
25
+ // Boards often glue the currency to the number ("10 000 000сум"); without this
26
+ // the boundary check fails on the full number and backtracks to "10 000".
27
+ const ATTACHED_CURRENCY = "сум|сўм|so['’ʻʼ`]?m|som|uzs|usd|eur|rub|руб|грн|kzt|kgs";
28
+ export const MONEY_SINGLE_RE = new RegExp(`(${MONEY_NUMBER_PATTERN})\\s*(${MONEY_SCALE_PATTERN})?(?=$|[^\\p{L}\\p{N}_]|(?:${ATTACHED_CURRENCY})(?![\\p{L}\\p{N}_]))`, 'giu');
26
29
 
27
30
  export function parseNumericAmount(raw) {
28
31
  let value = String(raw || '').replace(/\u00a0/g, ' ').trim();
package/src/money.js CHANGED
@@ -1,4 +1,4 @@
1
- import { findPhoneLikeSpans } from './contact.js';
1
+ import { findPhoneLikeSpans, parsePhoneNumbers } from './contact.js';
2
2
  import { aliasesOf, findCanonical, normalizeUnicode } from './normalization.js';
3
3
  import {
4
4
  CURRENCY_SYMBOL_CANDIDATES,
@@ -56,7 +56,15 @@ function hasModifier(text, key) {
56
56
  function protectedPhoneSpans(text) {
57
57
  return findPhoneLikeSpans(text).filter((span) => {
58
58
  const prefix = text.slice(Math.max(0, span.start - 32), span.start);
59
- return CONTACT_MARKER_RE.test(prefix);
59
+ if (CONTACT_MARKER_RE.test(prefix)) return true;
60
+ // A label-less international number ("998 953 506 090", typically scraped
61
+ // into a pay field) is a contact, not a 12-digit salary. Require a valid
62
+ // E.164 number of 11+ digits so ordinary grouped salaries stay money.
63
+ // A "998 95… - 998 95…" pair is merged into one span, so test each part.
64
+ return span.raw.split(/\s*[-–—]\s*/u).some((part) => {
65
+ const digits = part.replace(/\D/g, '');
66
+ return digits.length >= 11 && parsePhoneNumbers(`+${digits}`).length > 0;
67
+ });
60
68
  });
61
69
  }
62
70