@whiteslove/parsing-lexicon 0.9.8 → 0.9.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/README.md CHANGED
@@ -85,7 +85,44 @@ parseHiringContext(
85
85
  );
86
86
  ```
87
87
 
88
- Subpath exports are available for consumers that need narrower modules, including `./geo`, `./locations`, `./housing-context`, `./housing-structured`, `./hiring-context`, `./hiring-professions` and `./money`.
88
+ Subpath exports are available for consumers that need narrower modules, including `./geo`, `./locations`, `./housing-context`, `./housing-structured`, `./hiring-context`, `./hiring-professions`, `./housing-money`, `./money` and `./currency`.
89
+
90
+ ### Money & currency
91
+
92
+ Currency and magnitude vocabulary (symbols, ISO codes, spelled-out names, and
93
+ regional stand-ins like "у.е.") lives here once, in `money-lexicon.js`. A
94
+ consumer that needs to know whether some text mentions money at all — a fast
95
+ pre-filter before running the heavier parsers below, e.g. when scanning many
96
+ HTML card candidates scraped from a page — should use `moneyMentionPattern()`
97
+ rather than hand-copying a currency symbol list. A hand-copied list silently
98
+ drifts from this one; that exact bug (a missing "₸"/KZT symbol, and a missing
99
+ "у.е." token) is why this section exists.
100
+
101
+ ```js
102
+ import {
103
+ moneyCurrencyPattern, // just the currency alternation, e.g. for building a custom matcher
104
+ moneyMentionPattern, // currency OR a bare magnitude word ("15 млн") — boundary-guarded
105
+ moneyCurrencyFromText, // -> 'KZT' | 'USD' | ... | null
106
+ } from '@whiteslove/parsing-lexicon/currency';
107
+ import { parseHousingPrice } from '@whiteslove/parsing-lexicon/housing-money';
108
+ import { parseSalary } from '@whiteslove/parsing-lexicon/money';
109
+
110
+ const looksLikeMoney = new RegExp(moneyMentionPattern(), 'iu');
111
+ looksLikeMoney.test('300 000 ₸ в месяц'); // true
112
+ looksLikeMoney.test('2 до 3 месяцев'); // false — "м" inside "месяцев" does not count
113
+
114
+ moneyCurrencyFromText('300 000 ₸ в месяц'); // 'KZT'
115
+
116
+ parseHousingPrice('12 500 000 сум', 'UZS');
117
+ // -> { amount: 12500000, currency: 'UZS' }
118
+ ```
119
+
120
+ Do not write a source-local regex for "does this text contain a price" or
121
+ "what currency is this" — both already exist here and are kept in sync with
122
+ the full currency/magnitude vocabulary used by `parseHousingPrice`/
123
+ `parseSalary`. If a currency form you need is missing, add it to
124
+ `CURRENCY_TERMS` in `money-lexicon.js` (and `CURRENCY_SYMBOL_CANDIDATES` if
125
+ it's a standalone symbol) rather than working around the gap in the consumer.
89
126
 
90
127
  ## Data-quality rules
91
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
package/src/currency.d.ts CHANGED
@@ -9,6 +9,7 @@ export const CURRENCY_SYMBOL_CANDIDATES: Readonly<Record<string, readonly string
9
9
  export function moneyCurrencyCandidatesFromText(value: unknown): readonly string[];
10
10
  export function moneyCurrencyFromText(value: unknown, fallbackCurrency?: string | null): string | null;
11
11
  export function moneyCurrencyPattern(): string;
12
+ export function moneyMentionPattern(): string;
12
13
  export function currencyDisplay(code: unknown, locale?: string): CurrencyDisplay | null;
13
14
  export function currencySymbol(code: unknown, locale?: string): string | null;
14
15
  export function currencyName(code: unknown, locale?: string): string | null;
package/src/currency.js CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  moneyCurrencyCandidatesFromText,
10
10
  moneyCurrencyFromText,
11
11
  moneyCurrencyPattern,
12
+ moneyMentionPattern,
12
13
  } from './money-core.js';
13
14
 
14
15
  const LOCALE_ALIASES = Object.freeze({
@@ -19,16 +19,16 @@ import {
19
19
  } from './housing-numeric-spans.js';
20
20
 
21
21
  const NUMBER_WORDS = Object.freeze([
22
- [/(?<![\p{L}\p{N}_])(?:однушк\p{L}*|однокомнатн\p{L}*|bir\s+xona(?:li)?|бир\s+хона(?:ли|лик)?|1\s*(?:-\s*)?к(?:омн\p{L}*)?|1\s*(?:-\s*)?xona(?:li)?|1\s*(?:-\s*)?хона(?:лик|ли)?|1\s*бөлмелі|one[- ]bedroom|one[- ]room)(?![\p{L}\p{N}_])/iu, 1],
23
- [/(?<![\p{L}\p{N}_])(?:двушк\p{L}*|двухкомнатн\p{L}*|ikki\s+xona(?:li)?|икки\s+хона(?:ли|лик)?|2\s*(?:-\s*)?к(?:омн\p{L}*)?|2\s*(?:-\s*)?xona(?:li)?|2\s*(?:-\s*)?хона(?:лик|ли)?|2\s*бөлмелі|two[- ]bedroom|two[- ]room)(?![\p{L}\p{N}_])/iu, 2],
24
- [/(?<![\p{L}\p{N}_])(?:тр[её]шк\p{L}*|трехкомнатн\p{L}*|трёхкомнатн\p{L}*|uch\s+xona(?:li)?|уч\s+хона(?:ли|лик)?|3\s*(?:-\s*)?к(?:омн\p{L}*)?|3\s*(?:-\s*)?xona(?:li)?|3\s*(?:-\s*)?хона(?:лик|ли)?|3\s*бөлмелі|three[- ]bedroom|three[- ]room)(?![\p{L}\p{N}_])/iu, 3],
25
- [/(?<![\p{L}\p{N}_])(?:четыр[её]хкомнатн\p{L}*|четыр[её]шк\p{L}*|to['’]?rt\s+xona(?:li)?|tort\s+xona(?:li)?|тўрт\s+хона(?:ли|лик)?|турт\s+хона(?:ли|лик)?|4\s*(?:-\s*)?к(?:омн\p{L}*)?|4\s*(?:-\s*)?xona(?:li)?|4\s*(?:-\s*)?хона(?:лик|ли)?|4\s*бөлмелі|four[- ]bedroom|four[- ]room)(?![\p{L}\p{N}_])/iu, 4],
26
- [/(?<![\p{L}\p{N}_])(?:пятикомнатн\p{L}*|besh\s+xona(?:li)?|беш\s+хона(?:ли|лик)?|5\s*(?:-\s*)?к(?:омн\p{L}*)?|5\s*(?:-\s*)?xona(?:li)?|5\s*(?:-\s*)?хона(?:лик|ли)?|five[- ]bedroom|five[- ]room)(?![\p{L}\p{N}_])/iu, 5],
27
- [/(?<![\p{L}\p{N}_])(?:шестикомнатн\p{L}*|olti\s+xona(?:li)?|олти\s+хона(?:ли|лик)?|6\s*(?:-\s*)?к(?:омн\p{L}*)?|6\s*(?:-\s*)?xona(?:li)?|6\s*(?:-\s*)?хона(?:лик|ли)?|six[- ]bedroom|six[- ]room)(?![\p{L}\p{N}_])/iu, 6],
28
- [/(?<![\p{L}\p{N}_])(?:семикомнатн\p{L}*|yetti\s+xona(?:li)?|етти\s+хона(?:ли|лик)?|7\s*(?:-\s*)?к(?:омн\p{L}*)?|7\s*(?:-\s*)?xona(?:li)?|7\s*(?:-\s*)?хона(?:лик|ли)?|seven[- ]bedroom|seven[- ]room)(?![\p{L}\p{N}_])/iu, 7],
29
- [/(?<![\p{L}\p{N}_])(?:восьмикомнатн\p{L}*|sakkiz\s+xona(?:li)?|саккиз\s+хона(?:ли|лик)?|8\s*(?:-\s*)?к(?:омн\p{L}*)?|8\s*(?:-\s*)?xona(?:li)?|8\s*(?:-\s*)?хона(?:лик|ли)?|eight[- ]bedroom|eight[- ]room)(?![\p{L}\p{N}_])/iu, 8],
30
- [/(?<![\p{L}\p{N}_])(?:девятикомнатн\p{L}*|to['’]?qqiz\s+xona(?:li)?|toqqiz\s+xona(?:li)?|тўққиз\s+хона(?:ли|лик)?|токкиз\s+хона(?:ли|лик)?|9\s*(?:-\s*)?к(?:омн\p{L}*)?|9\s*(?:-\s*)?xona(?:li)?|9\s*(?:-\s*)?хона(?:лик|ли)?|nine[- ]bedroom|nine[- ]room)(?![\p{L}\p{N}_])/iu, 9],
31
- [/(?<![\p{L}\p{N}_])(?:десятикомнатн\p{L}*|o['’]?n\s+xona(?:li)?|on\s+xona(?:li)?|ўн\s+хона(?:ли|лик)?|он\s+хона(?:ли|лик)?|10\s*(?:-\s*)?к(?:омн\p{L}*)?|10\s*(?:-\s*)?xona(?:li)?|10\s*(?:-\s*)?хона(?:лик|ли)?|ten[- ]bedroom|ten[- ]room)(?![\p{L}\p{N}_])/iu, 10],
22
+ [/(?<![\p{L}\p{N}_])(?:однушк\p{L}*|однокомнатн\p{L}*|однокімнатн\p{L}*|bir\s+xona(?:li)?|бир\s+хона(?:ли|лик)?|1\s*(?:-\s*)?к(?:омн\p{L}*)?|1\s*(?:-\s*)?xona(?:li)?|1\s*(?:-\s*)?хона(?:лик|ли)?|1\s*бөлмелі|one[- ]bedroom|one[- ]room)(?![\p{L}\p{N}_])/iu, 1],
23
+ [/(?<![\p{L}\p{N}_])(?:двушк\p{L}*|двухкомнатн\p{L}*|двокімнатн\p{L}*|ikki\s+xona(?:li)?|икки\s+хона(?:ли|лик)?|2\s*(?:-\s*)?к(?:омн\p{L}*)?|2\s*(?:-\s*)?xona(?:li)?|2\s*(?:-\s*)?хона(?:лик|ли)?|2\s*бөлмелі|two[- ]bedroom|two[- ]room)(?![\p{L}\p{N}_])/iu, 2],
24
+ [/(?<![\p{L}\p{N}_])(?:тр[её]шк\p{L}*|трехкомнатн\p{L}*|трёхкомнатн\p{L}*|трикімнатн\p{L}*|uch\s+xona(?:li)?|уч\s+хона(?:ли|лик)?|3\s*(?:-\s*)?к(?:омн\p{L}*)?|3\s*(?:-\s*)?xona(?:li)?|3\s*(?:-\s*)?хона(?:лик|ли)?|3\s*бөлмелі|three[- ]bedroom|three[- ]room)(?![\p{L}\p{N}_])/iu, 3],
25
+ [/(?<![\p{L}\p{N}_])(?:четыр[её]хкомнатн\p{L}*|четыр[её]шк\p{L}*|чотирикімнатн\p{L}*|to['’]?rt\s+xona(?:li)?|tort\s+xona(?:li)?|тўрт\s+хона(?:ли|лик)?|турт\s+хона(?:ли|лик)?|4\s*(?:-\s*)?к(?:омн\p{L}*)?|4\s*(?:-\s*)?xona(?:li)?|4\s*(?:-\s*)?хона(?:лик|ли)?|4\s*бөлмелі|four[- ]bedroom|four[- ]room)(?![\p{L}\p{N}_])/iu, 4],
26
+ [/(?<![\p{L}\p{N}_])(?:пятикомнатн\p{L}*|п['’]ятикімнатн\p{L}*|besh\s+xona(?:li)?|беш\s+хона(?:ли|лик)?|5\s*(?:-\s*)?к(?:омн\p{L}*)?|5\s*(?:-\s*)?xona(?:li)?|5\s*(?:-\s*)?хона(?:лик|ли)?|five[- ]bedroom|five[- ]room)(?![\p{L}\p{N}_])/iu, 5],
27
+ [/(?<![\p{L}\p{N}_])(?:шестикомнатн\p{L}*|шестикімнатн\p{L}*|olti\s+xona(?:li)?|олти\s+хона(?:ли|лик)?|6\s*(?:-\s*)?к(?:омн\p{L}*)?|6\s*(?:-\s*)?xona(?:li)?|6\s*(?:-\s*)?хона(?:лик|ли)?|six[- ]bedroom|six[- ]room)(?![\p{L}\p{N}_])/iu, 6],
28
+ [/(?<![\p{L}\p{N}_])(?:семикомнатн\p{L}*|семикімнатн\p{L}*|yetti\s+xona(?:li)?|етти\s+хона(?:ли|лик)?|7\s*(?:-\s*)?к(?:омн\p{L}*)?|7\s*(?:-\s*)?xona(?:li)?|7\s*(?:-\s*)?хона(?:лик|ли)?|seven[- ]bedroom|seven[- ]room)(?![\p{L}\p{N}_])/iu, 7],
29
+ [/(?<![\p{L}\p{N}_])(?:восьмикомнатн\p{L}*|восьмикімнатн\p{L}*|sakkiz\s+xona(?:li)?|саккиз\s+хона(?:ли|лик)?|8\s*(?:-\s*)?к(?:омн\p{L}*)?|8\s*(?:-\s*)?xona(?:li)?|8\s*(?:-\s*)?хона(?:лик|ли)?|eight[- ]bedroom|eight[- ]room)(?![\p{L}\p{N}_])/iu, 8],
30
+ [/(?<![\p{L}\p{N}_])(?:девятикомнатн\p{L}*|дев['’]ятикімнатн\p{L}*|to['’]?qqiz\s+xona(?:li)?|toqqiz\s+xona(?:li)?|тўққиз\s+хона(?:ли|лик)?|токкиз\s+хона(?:ли|лик)?|9\s*(?:-\s*)?к(?:омн\p{L}*)?|9\s*(?:-\s*)?xona(?:li)?|9\s*(?:-\s*)?хона(?:лик|ли)?|nine[- ]bedroom|nine[- ]room)(?![\p{L}\p{N}_])/iu, 9],
31
+ [/(?<![\p{L}\p{N}_])(?:десятикомнатн\p{L}*|десятикімнатн\p{L}*|o['’]?n\s+xona(?:li)?|on\s+xona(?:li)?|ўн\s+хона(?:ли|лик)?|он\s+хона(?:ли|лик)?|10\s*(?:-\s*)?к(?:омн\p{L}*)?|10\s*(?:-\s*)?xona(?:li)?|10\s*(?:-\s*)?хона(?:лик|ли)?|ten[- ]bedroom|ten[- ]room)(?![\p{L}\p{N}_])/iu, 10],
32
32
  ]);
33
33
 
34
34
  // Common classifieds shorthand: rooms/floor/total floors, optionally followed
package/src/housing.js CHANGED
@@ -210,7 +210,7 @@ export function looksHousingRoomOnly(value) {
210
210
  if (!text) return false;
211
211
  const occupancy = resolveHousingOccupancy(text);
212
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);
213
+ return /подселени|підселен|комнату\s+в(?![\p{L}\p{N}_])|кімнату\s+в(?![\p{L}\p{N}_])|сда[её]тся\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
214
  }
215
215
 
216
216
  export function resolveHousingPropertyType(value) {
@@ -8,3 +8,4 @@ export function parseScaledAmount(raw: unknown, scale?: unknown): number | null;
8
8
  export function moneyCurrencyCandidatesFromText(value: unknown): readonly string[];
9
9
  export function moneyCurrencyFromText(value: unknown, fallbackCurrency?: string | null): string | null;
10
10
  export function moneyCurrencyPattern(): string;
11
+ export function moneyMentionPattern(): string;
package/src/money-core.js CHANGED
@@ -112,3 +112,19 @@ export function moneyCurrencyPattern() {
112
112
  .map(escapeRegex)
113
113
  .join('|');
114
114
  }
115
+
116
+ // A cheap "does this text mention money at all" pre-filter — a currency term
117
+ // or a bare magnitude word (e.g. "15 млн", no currency spelled out) both
118
+ // count. Consumers that need to reject non-money text fast before running a
119
+ // full parseSalary/parseHousingPrice pass (e.g. scanning many HTML card
120
+ // candidates) can test against this instead of hand-copying a currency list,
121
+ // which drifts from the lexicon's own (see MONEY_SCALE_PATTERN for the
122
+ // magnitude half already used by MONEY_RANGE_RE/MONEY_SINGLE_RE).
123
+ //
124
+ // Word-boundary guarded for the same reason MONEY_RANGE_RE/MONEY_SINGLE_RE
125
+ // are: MONEY_SCALE_PATTERN includes bare single-letter abbreviations like "м"
126
+ // (million) that would otherwise match inside an unrelated word — e.g. "2 до
127
+ // 3 месяцев" ("months") must never register as a money mention.
128
+ export function moneyMentionPattern() {
129
+ return `(?<![\\p{L}\\p{N}_])(?:${moneyCurrencyPattern()}|${MONEY_SCALE_PATTERN})(?![\\p{L}\\p{N}_])`;
130
+ }
@@ -6,7 +6,7 @@ export const CURRENCY_TERMS = Object.freeze([
6
6
  group('USD', { ru: ['$', 'usd', 'доллар', 'доллара', 'долларов', 'бакс', 'у.е.', 'у е'], en: ['$', 'usd', 'dollar', 'dollars', 'us dollar'], uk: ['$', 'usd', 'долар', 'долари', 'доларів'], ro: ['$', 'usd', 'dolar', 'dolari'], uzLatn: ['$', 'usd', 'dollar'], uzCyrl: ['$', 'доллар'], kk: ['$', 'usd', 'доллар'] }),
7
7
  group('EUR', { ru: ['€', 'eur', 'евро'], en: ['€', 'eur', 'euro', 'euros'], uk: ['€', 'eur', 'євро'], ro: ['€', 'eur', 'euro'], uzLatn: ['€', 'eur', 'yevro'], uzCyrl: ['€', 'евро'], kk: ['€', 'eur', 'еуро'] }),
8
8
  group('UZS', { ru: ['сум', 'сумов', 'узс'], en: ['uzs', 'uzbek som'], uk: ['uzs', 'сум'], ro: ['uzs'], uzLatn: ["so'm", 'so‘m', 'soʻm', 'sum', 'uzs'], uzCyrl: ['сўм', 'сум'], kk: ['uzs'] }),
9
- group('KZT', { ru: ['тенге', 'тг', 'kzt'], en: ['kzt', 'tenge'], uk: ['тенге', 'kzt'], ro: ['kzt', 'tenge'], uzLatn: ['kzt', 'tenge'], uzCyrl: ['kzt', 'тенге'], kk: ['теңге', 'тенге', 'тг', 'kzt'] }),
9
+ group('KZT', { ru: ['₸', 'тенге', 'тг', 'kzt'], en: ['₸', 'kzt', 'tenge'], uk: ['₸', 'тенге', 'kzt'], ro: ['₸', 'kzt', 'tenge'], uzLatn: ['₸', 'kzt', 'tenge'], uzCyrl: ['₸', 'kzt', 'тенге'], kk: ['₸', 'теңге', 'тенге', 'тг', 'kzt'] }),
10
10
  group('UAH', { ru: ['грн', 'гривна', 'гривны', 'гривен', '₴', 'uah'], en: ['uah', 'hryvnia', 'hryvnias', '₴'], uk: ['грн', 'гривня', 'гривні', 'гривень', '₴', 'uah'], ro: ['uah', 'hrivne', 'grivne'], uzLatn: ['uah'], uzCyrl: ['uah'], kk: ['uah'] }),
11
11
  group('RUB', { ru: ['₽', 'руб', 'руб.', 'рубль', 'рубля', 'рублей', 'rub', 'rur'], en: ['₽', 'rub', 'rur', 'ruble', 'rubles'], uk: ['₽', 'руб', 'рублів', 'rub'], ro: ['₽', 'rub', 'ruble'], uzLatn: ['₽', 'rub', 'rubl'], uzCyrl: ['₽', 'рубль'], kk: ['₽', 'rub', 'рубль'] }),
12
12
  group('GBP', { ru: ['£', 'gbp', 'фунт', 'фунтов'], en: ['£', 'gbp', 'pound', 'pounds', 'sterling'], uk: ['£', 'gbp', 'фунт', 'фунтів'], ro: ['£', 'gbp', 'liră sterlină', 'lira sterlina'], uzLatn: ['£', 'gbp', 'funt'], uzCyrl: ['£', 'фунт'], kk: ['£', 'gbp', 'фунт'] }),
@@ -41,6 +41,7 @@ export const CURRENCY_SYMBOL_CANDIDATES = Object.freeze({
41
41
  '€': Object.freeze(['EUR']),
42
42
  '₴': Object.freeze(['UAH']),
43
43
  '₽': Object.freeze(['RUB']),
44
+ '₸': Object.freeze(['KZT']),
44
45
  '£': Object.freeze(['GBP']),
45
46
  '₺': Object.freeze(['TRY']),
46
47
  '₾': Object.freeze(['GEL']),
package/src/money.js CHANGED
@@ -22,6 +22,7 @@ export {
22
22
  MONEY_SINGLE_RE,
23
23
  moneyCurrencyFromText,
24
24
  moneyCurrencyPattern,
25
+ moneyMentionPattern,
25
26
  moneyScaleMultiplier,
26
27
  parseNumericAmount,
27
28
  parseScaledAmount,