@whiteslove/parsing-lexicon 0.6.9 → 0.6.10

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.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -188,7 +188,7 @@ const rows = [
188
188
  ['chat_operator','customer_support',['chat operator','оператор чата','чат оператор']],
189
189
  ['customer_success_manager','customer_support',['customer success manager','csm','менеджер по успеху клиентов']],
190
190
  ['office_manager','administration',['office manager','офис менеджер','офіс менеджер']],
191
- ['administrator','administration',['administrator','администратор','адміністратор','administrator']],
191
+ ['administrator','administration',['administrator','администратор','администратором','адміністратор','адміністратором']],
192
192
  ['receptionist','administration',['receptionist','ресепшионист','ресепшн','рецепционист']],
193
193
  ['executive_assistant','administration',['executive assistant','personal assistant','ассистент руководителя','личный помощник']],
194
194
  ['secretary','administration',['secretary','секретарь','секретар']],
@@ -1,6 +1,6 @@
1
1
  import type { DegreeLevel } from './hiring-requirements.js';
2
2
 
3
- export type CandidateFeatureCode = 'student' | 'parentalLeave' | 'noExperience' | 'partTime' | 'nightShift' | 'openToRelocation';
3
+ export type CandidateFeatureCode = 'student' | 'parentalLeave' | 'noExperience' | 'partTime' | 'nightShift' | 'openToRelocation' | 'withPets';
4
4
  export type HiringScopeCode = 'architecture' | 'leadership' | 'mentoring' | 'scale' | 'ownership';
5
5
 
6
6
  export function isCandidateNonTargetContext(value: unknown): boolean;
@@ -31,6 +31,7 @@ const FEATURE_RULES = Object.freeze([
31
31
  ['partTime', /подработк|підробіт|part[-\s]?time|неполный\s+день|неповн(?:ий|а)\s+день|yarim\s+stavka/iu],
32
32
  ['nightShift', /ночн(?:ая|ую|ой)\s+смен|нічн(?:а|у|ої)\s+змін|night\s+shift|tungi\s+smena/iu],
33
33
  ['openToRelocation', /готов\p{L}*\s+к\s+переезду|готов\p{L}*\s+переехать|готов\p{L}*\s+до\s+переїзду|relocat(?:e|ion)|ko(?:'|’)chib\s+o(?:'|’)tish/iu],
34
+ ['withPets', /тварин\p{L}*|животн\p{L}*|жануар\p{L}*|\bwith\s+(?:animals|pets)\b|\banimals?\b|\bpets?\b|hayvon(?:lar)?(?:\s+bilan)?/iu],
34
35
  ]);
35
36
 
36
37
  const RELOCATION_NEGATIVE_RE = /не\s+готов\p{L}*\s+к\s+переезду|не\s+розгляда\p{L}*\s+переїзд|not\s+(?:open|ready)\s+to\s+relocat/iu;
@@ -149,7 +149,7 @@ export const SKILL_CATALOG = [
149
149
  ...group('Marketing', 'Digital Marketing', [
150
150
  ['Digital Marketing'], ['Content Marketing'], ['Social Media Marketing', ['smm']], ['Email Marketing'],
151
151
  ['Google Ads', ['google adwords']], ['Meta Ads', ['facebook ads']], ['Google Analytics', ['ga4']],
152
- ['Google Tag Manager', ['gtm']], ['Marketing Automation'], ['Copywriting', ['копирайтинг']],
152
+ ['Google Tag Manager'], ['Marketing Automation'], ['Copywriting', ['копирайтинг']],
153
153
  ['Market Research'], ['A/B Testing', ['ab testing']], ['Conversion Rate Optimization', ['cro']],
154
154
  ]),
155
155
  ...group('HR', 'Recruiting & People', [
@@ -24,6 +24,10 @@ const FIELD_EXTRA_ALIASES = Object.freeze({
24
24
  const APPLICATION_MODAL_RE = /(?:отправить\s*резюме\s*)+(?:\[telegram\]\s*)?пол\s*мужской\s*женский\s*образование[\s\S]{0,500}?выберите\s*вакансию[\s\S]*/iu;
25
25
  const LEAKED_STYLESHEET_START_RE = /:root\s*\{\s*--[\w-]+\s*:/iu;
26
26
  const LEAKED_CSS_RULE_RE = /\s*(?:[.#][\w\\-]+|:root)(?:[^{}\n]{0,300})\{\s*(?:(?:--)?[\w-]+\s*:\s*[^;{}]+;?\s*)+\}\s*/giu;
27
+ // Button/chrome labels copied along with a resume from Ukrainian job boards
28
+ // (novarobota.ua, robota.ua, work.ua and similar). Matched as whole lines so
29
+ // genuine resume prose is never touched.
30
+ const JOB_BOARD_UI_NOISE_RE = /^(?:подробнее|детальніше|предпросмотр|попередній\s+перегляд|добавить\s+в\s+отклики\/?\s*избранное|додати\s+до\s+відгуків\/?\s*обраного|предложить\s+вакансию|запропонувати\s+вакансію|для\s+того\s+чтобы\s+предложить\s+вакансию\s+нужно\s+авторизоваться\s+как\s+работодатель|щоб\s+запропонувати\s+вакансію[^\n]*|пожаловаться\s+на\s+резюме|поскаржитися\s+на\s+резюме|отправить)$/iu;
27
31
 
28
32
  /** Remove source-page controls and stylesheet fragments copied into hiring text. */
29
33
  export function cleanHiringSourceText(value) {
@@ -32,7 +36,11 @@ export function cleanHiringSourceText(value) {
32
36
  return (stylesheetStart >= 0 ? text.slice(0, stylesheetStart) : text)
33
37
  .replace(APPLICATION_MODAL_RE, '')
34
38
  .replace(LEAKED_CSS_RULE_RE, ' ')
39
+ .split('\n')
40
+ .filter((line) => !JOB_BOARD_UI_NOISE_RE.test(line.trim()))
41
+ .join('\n')
35
42
  .replace(/[ \t]+(?=\n)/g, '')
43
+ .replace(/\n{3,}/g, '\n\n')
36
44
  .trim();
37
45
  }
38
46
 
@@ -12,10 +12,10 @@ const HOUSE_MARKER = String.raw`(?:дом|д\.|будинок|буд\.|house|h\.
12
12
  const BUILDING_MARKER = String.raw`(?:корп(?:ус)?\.?|к\.|строен(?:ие)?|стр\.|будова|секц(?:ия|ія)?|bloc|corp|building|bldg\.?|korpus)`;
13
13
  const NUMBER_TOKEN = String.raw`\d{1,5}(?:[-\/]?[\p{L}])?(?:[\/-]\d{1,4}(?:[-\/]?[\p{L}])?)?`;
14
14
  const STREET_WORD = String.raw`[\p{L}'’.-]{2,48}`;
15
- const ADDRESS_FIELD_STOP_RE = /\s+(?:цена|ціна|нарх(?:и)?|narx|price|стоимост[ьи]|этаж(?:ность)?|поверх|qavat|қабат|кават|қават|комнат(?:ы|а)?|кімнат(?:и|а)?|xona|хона|площадь|площа|maydon|тел(?:ефон)?|phone|комисси\p{L}*|депозит|deposit)(?=$|[\s:№#-])/iu;
15
+ const ADDRESS_FIELD_STOP_RE = /\s+(?:цена|ціна|нарх(?:и)?|narx|price|стоимост[ьи]|этаж(?:ность)?|поверх|qavat|қабат|кават|қават|комнат(?:ы|а)?|кімнат(?:и|а)?|xona|хона|площадь|площа|maydon|тел(?:ефон)?|phone|комисси\p{L}*|депозит|deposit|ориентир\p{L}*|ор[-–—]?р\.?)(?=$|[\s:№#-])/iu;
16
16
  const PROPERTY_AREA_LINE_RE = /(?:^|[^\p{L}\p{N}_])(?:(?:общая|жилая|полезная|кухонная)\s+площадь|площадь\s+(?:квартиры|дома|комнаты))(?=$|[^\p{L}\p{N}_])/iu;
17
17
  const NON_ADDRESS_BARE_RE = /^(?:(?:(?:перш(?:ий|ому)|перв(?:ый|ом)|друг(?:ий|ому)|втор(?:ой|ом)|трет(?:ій|ьем|ий)|\d{1,3}(?:-?й)?)\s+(?:поверх|этаж|floor|qavat|қабат))|(?:поверх|этаж|floor|qavat|қабат)(?:\s|$)|(?:район|р-н|рн|мікрорайон|микрорайон|мкр\.?|жк|ж\.к\.|жилой\s+комплекс|житловий\s+комплекс|residential\s+complex)(?:\s|$)|(?:недалеко|поруч|рядом|біля|около|возле)(?=$|[^\p{L}\p{N}_])|(?:зупинка|остановка|станція|станция)(?:\s|$))/iu;
18
- const DELIMITED_STREET_REJECT_RE = /(?:^|\s)(?:город|місто|city|район|р-н|рн|мікрорайон|микрорайон|мкр|жк|метро|поверх|этаж|floor|qavat|кімнат\p{L}*|комнат\p{L}*|квартира|квартири|квартиры|оренда|аренда|продаж\p{L}*|цена|ціна|площад\p{L}*|площа|зупинка|остановка)(?:\s|$)/iu;
18
+ const DELIMITED_STREET_REJECT_RE = /(?:^|\s)(?:город|місто|city|район|р-н|рн|мікрорайон|микрорайон|мкр|жк|метро|поверх|этаж|floor|qavat|кімнат\p{L}*|комнат\p{L}*|квартира|квартири|квартиры|оренда|аренда|продаж\p{L}*|цена|ціна|площад\p{L}*|площа|зупинка|остановка|ориентир\p{L}*|ор[-–—]?р\.?)(?:\s|$)/iu;
19
19
 
20
20
  function clean(value) {
21
21
  return String(value ?? '')
@@ -265,11 +265,14 @@ function delimitedBareAddress(text) {
265
265
  function bareAddress(text) {
266
266
  const cleaned = clean(text);
267
267
  if (!cleaned || PROPERTY_AREA_LINE_RE.test(cleaned) || NON_ADDRESS_BARE_RE.test(cleaned)) return null;
268
- const tail = splitAddressTail(cleaned);
268
+ const stopMatch = cleaned.match(ADDRESS_FIELD_STOP_RE);
269
+ const truncated = stopMatch ? clean(cleaned.slice(0, stopMatch.index)) : cleaned;
270
+ if (!truncated) return null;
271
+ const tail = splitAddressTail(truncated);
269
272
  if (!tail) return null;
270
273
  if (/^(?:центр|centre|center)$/iu.test(compactStreet(tail.street) || '')) return null;
271
274
  const confidence = tail.houseNumber ? 0.85 : 0.55;
272
- return result(cleaned, tail.street, tail.houseNumber, tail.building, confidence);
275
+ return result(truncated, tail.street, tail.houseNumber, tail.building, confidence);
273
276
  }
274
277
 
275
278
  /**