@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.
Files changed (94) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +137 -137
  3. package/index.d.ts +374 -374
  4. package/package.json +77 -75
  5. package/src/central-asia-locations.js +211 -211
  6. package/src/central-asia.js +233 -233
  7. package/src/contact.d.ts +44 -44
  8. package/src/contact.js +159 -159
  9. package/src/countries.js +175 -175
  10. package/src/country-context.d.ts +10 -10
  11. package/src/country-context.js +22 -22
  12. package/src/currency.d.ts +14 -14
  13. package/src/currency.js +66 -66
  14. package/src/geo.js +169 -169
  15. package/src/geography-central-asia.js +42 -42
  16. package/src/geography-detection.d.ts +14 -14
  17. package/src/geography-detection.js +64 -64
  18. package/src/geography-display.d.ts +13 -13
  19. package/src/geography-display.js +113 -113
  20. package/src/geography.js +210 -210
  21. package/src/hiring-advanced.js +147 -147
  22. package/src/hiring-candidate-fields.d.ts +9 -9
  23. package/src/hiring-candidate-fields.js +92 -92
  24. package/src/hiring-context.d.ts +64 -64
  25. package/src/hiring-context.js +412 -412
  26. package/src/hiring-language-extensions.d.ts +19 -19
  27. package/src/hiring-language-extensions.js +139 -139
  28. package/src/hiring-languages.js +135 -135
  29. package/src/hiring-location-fields.d.ts +3 -3
  30. package/src/hiring-location-fields.js +53 -53
  31. package/src/hiring-professions-ro.js +191 -191
  32. package/src/hiring-professions.js +437 -437
  33. package/src/hiring-requirements.d.ts +16 -16
  34. package/src/hiring-requirements.js +170 -170
  35. package/src/hiring-salary-context.d.ts +27 -27
  36. package/src/hiring-salary-context.js +76 -76
  37. package/src/hiring-semantics.d.ts +29 -29
  38. package/src/hiring-semantics.js +218 -218
  39. package/src/hiring-skills.d.ts +21 -21
  40. package/src/hiring-skills.js +315 -315
  41. package/src/hiring-source-aliases.d.ts +32 -32
  42. package/src/hiring-source-aliases.js +185 -185
  43. package/src/hiring-source-semantics.d.ts +31 -31
  44. package/src/hiring-source-semantics.js +293 -293
  45. package/src/hiring-temporal.d.ts +15 -15
  46. package/src/hiring-temporal.js +67 -67
  47. package/src/hiring-vacancy-fields.d.ts +5 -5
  48. package/src/hiring-vacancy-fields.js +23 -23
  49. package/src/hiring-work-semantics.d.ts +34 -34
  50. package/src/hiring-work-semantics.js +72 -72
  51. package/src/hiring.js +180 -180
  52. package/src/housing-address.d.ts +18 -18
  53. package/src/housing-address.js +217 -210
  54. package/src/housing-commercial.d.ts +1 -0
  55. package/src/housing-commercial.js +9 -0
  56. package/src/housing-context.d.ts +36 -36
  57. package/src/housing-context.js +183 -183
  58. package/src/housing-features.js +30 -30
  59. package/src/housing-intent.d.ts +9 -9
  60. package/src/housing-intent.js +132 -132
  61. package/src/housing-listing-fields.js +131 -131
  62. package/src/housing-money.d.ts +7 -7
  63. package/src/housing-money.js +102 -102
  64. package/src/housing-source-aliases.d.ts +8 -8
  65. package/src/housing-source-aliases.js +51 -51
  66. package/src/housing-structured.d.ts +55 -55
  67. package/src/housing-structured.js +219 -219
  68. package/src/housing.js +200 -200
  69. package/src/index.js +45 -44
  70. package/src/kz-location-extensions.js +307 -307
  71. package/src/landmarks.js +21 -21
  72. package/src/lexicon-core.js +58 -58
  73. package/src/location-merge.js +106 -106
  74. package/src/locations.js +456 -456
  75. package/src/money-core.d.ts +10 -10
  76. package/src/money-core.js +81 -81
  77. package/src/money-lexicon.d.ts +5 -5
  78. package/src/money-lexicon.js +79 -79
  79. package/src/money.js +154 -154
  80. package/src/normalization.js +384 -384
  81. package/src/odesa-metropolitan.js +144 -144
  82. package/src/romania-geography.js +47 -47
  83. package/src/tashkent-colloquial.js +80 -72
  84. package/src/tashkent-housing-geography.d.ts +38 -38
  85. package/src/tashkent-housing-geography.js +211 -211
  86. package/src/tashkent-pois.js +215 -215
  87. package/src/tashkent-residential-complexes.js +272 -272
  88. package/src/ua-location-extensions-major.js +118 -118
  89. package/src/ua-location-extensions-metro.js +9 -9
  90. package/src/ua-location-extensions-regional.js +246 -246
  91. package/src/ua-secondary-cities.d.ts +2 -2
  92. package/src/ua-secondary-cities.js +57 -57
  93. package/src/ukraine.js +75 -75
  94. package/src/uz-location-extensions.js +259 -259
@@ -1,32 +1,32 @@
1
- export interface ExtendedProfessionMatch {
2
- canonical: string;
3
- group: string;
4
- family: string;
5
- score: number;
6
- strength: 'strong' | 'weak';
7
- matched: string;
8
- index: number;
9
- label: string;
10
- }
11
-
12
- export interface SourceProfessionAlias {
13
- canonical: string;
14
- label: string;
15
- group: string;
16
- aliases: readonly string[];
17
- re: RegExp;
18
- }
19
-
20
- export interface SourceRoleNormalization {
21
- canonical: string;
22
- label: string;
23
- re: RegExp;
24
- }
25
-
26
- export const SOURCE_PROFESSION_ALIASES: readonly SourceProfessionAlias[];
27
- export const SOURCE_ROLE_NORMALIZATION_RULES: readonly SourceRoleNormalization[];
28
- export const SOURCE_CANDIDATE_INTENT_ALIASES: readonly string[];
29
- export function normalizeSourceRole(value: unknown): Readonly<{ canonical: string; label: string }> | null;
30
- export function matchesSourceCandidateIntent(value: unknown): boolean;
31
- export function professionDisplayLabel(canonical: string): string;
32
- export function matchExtendedProfessions(value: unknown, options?: { limit?: number; allowWeak?: boolean }): readonly ExtendedProfessionMatch[];
1
+ export interface ExtendedProfessionMatch {
2
+ canonical: string;
3
+ group: string;
4
+ family: string;
5
+ score: number;
6
+ strength: 'strong' | 'weak';
7
+ matched: string;
8
+ index: number;
9
+ label: string;
10
+ }
11
+
12
+ export interface SourceProfessionAlias {
13
+ canonical: string;
14
+ label: string;
15
+ group: string;
16
+ aliases: readonly string[];
17
+ re: RegExp;
18
+ }
19
+
20
+ export interface SourceRoleNormalization {
21
+ canonical: string;
22
+ label: string;
23
+ re: RegExp;
24
+ }
25
+
26
+ export const SOURCE_PROFESSION_ALIASES: readonly SourceProfessionAlias[];
27
+ export const SOURCE_ROLE_NORMALIZATION_RULES: readonly SourceRoleNormalization[];
28
+ export const SOURCE_CANDIDATE_INTENT_ALIASES: readonly string[];
29
+ export function normalizeSourceRole(value: unknown): Readonly<{ canonical: string; label: string }> | null;
30
+ export function matchesSourceCandidateIntent(value: unknown): boolean;
31
+ export function professionDisplayLabel(canonical: string): string;
32
+ export function matchExtendedProfessions(value: unknown, options?: { limit?: number; allowWeak?: boolean }): readonly ExtendedProfessionMatch[];
@@ -1,186 +1,186 @@
1
- import { aliasesToRegex } from './normalization.js';
2
- import { matchProfessions } from './hiring-professions.js';
3
-
4
- const sourceRole = (canonical, label, aliases, group = 'other') => Object.freeze({
5
- canonical,
6
- label,
7
- group,
8
- aliases: Object.freeze([...new Set(aliases)]),
9
- re: aliasesToRegex([...new Set(aliases)]),
10
- });
11
-
12
- /**
13
- * Source spellings seen in real CV feeds which are too source-specific or
14
- * colloquial for the core profession rows. They still live in the shared
15
- * package so consumers do not grow their own profession regex catalogs.
16
- */
17
- export const SOURCE_PROFESSION_ALIASES = Object.freeze([
18
- sourceRole('chief_executive_officer', 'Chief Executive Officer', ['ceo', 'chief executive officer', 'генеральный директор', 'гендиректор', 'виконавчий директор'], 'management'),
19
- sourceRole('chief_technology_officer', 'Chief Technology Officer', ['cto', 'chief technology officer', 'технический директор', 'технічний директор'], 'management'),
20
- sourceRole('sales_manager', 'Sales Manager', ['sales executive', 'head of sales', 'менеджер экспортных продаж', 'менеджер по экспортным продажам', 'роп', 'sotuv menejer'], 'sales'),
21
- sourceRole('driver', 'Driver', ['се категория', 'се категория буйича', 'ce категория', 'ce category', 'shafyor'], 'transport'),
22
- sourceRole('accountant', 'Accountant', ['bugalteriya', 'buxgalteriya', 'buhgalteriya'], 'finance'),
23
- sourceRole('cashier', 'Cashier', ['kassa xodimi', 'kassa mudiri'], 'retail'),
24
- sourceRole('notary', 'Notary', ['notary', 'notarius', 'нотариус'], 'legal'),
25
- sourceRole('metrology_specialist', 'Metrology Specialist', ['metrologiya', 'metrology specialist', 'метролог', 'standartlashtirish'], 'manufacturing'),
26
- sourceRole('security_guard', 'Security Guard', ['xavfsizlik', 'qoriqlash', 'qo‘riqlash', "qo'riqlash", 'охорона'], 'security'),
27
- sourceRole('finance_banking_specialist', 'Finance / Banking Specialist', ['finance specialist', 'banking specialist', 'moliya', 'soliq', 'bank'], 'finance'),
28
- sourceRole('teacher', 'Teacher', ['tyutorlik', 'тьютор', 'titur', "o'qituvchilik", 'o‘qituvchilik', 'oʻqituvchilik', 'ustoz'], 'education'),
29
- sourceRole('english_teacher', 'English Teacher', ['ingliz tili ustoziman', 'ingliz tili ustoz', 'ingliz tili oqituvchi', "ingliz tili o'qituvchi"], 'education'),
30
- sourceRole('it_specialist', 'IT Specialist', ['it specialist', 'itishnik', 'it ishnik', 'kompyuter boyicha ish', "kompyuter bo'yicha ish", 'kompyuter bo‘yicha ish'], 'infrastructure'),
31
- sourceRole('biotechnologist', 'Biotechnologist', ['biotechnologist', 'биотехнолог', 'biotexnolog'], 'medicine'),
32
- sourceRole('laboratory_technician', 'Laboratory Technician', ['laborant'], 'medicine'),
33
- sourceRole('media_specialist', 'Media Specialist', ['media specialist', 'специалист по сми', 'matbuot'], 'media_content'),
34
- sourceRole('engineer', 'Engineer', ['engineer', 'инженер', 'інженер', 'muhandis', 'injiner'], 'engineering'),
35
- sourceRole('chat_operator', 'Chat Operator', ['chat operatori'], 'customer_support'),
36
- sourceRole('call_center_operator', 'Call Center Operator', ['koll-markaz operatori', 'koll markaz operatori', 'call-markaz operatori'], 'customer_support'),
37
- sourceRole('consultant', 'Consultant', ['consultant', 'консультант', 'консультантка'], 'commercial'),
38
- sourceRole('supervisor', 'Supervisor', ['supervisor', 'супервайзер', 'начальник отряд', 'начальник отряда'], 'management'),
39
- sourceRole('quality_inspector', 'Quality Inspector', ['quality inspector', 'инспектор по качеству', 'інспектор з якості'], 'manufacturing'),
40
- sourceRole('oil_gas_worker', 'Oil & Gas Worker', ['oil and gas', 'oil & gas', 'нефть и газ', 'нефтегаз', 'neft va gaz', 'neft vagaz', 'neft vagaz sohasida', 'neft va gaz sohasida'], 'manufacturing'),
41
- sourceRole('cybersecurity_specialist', 'Cybersecurity Specialist', ['cybersecurity specialist', 'ciso', 'руководитель по информационной безопасности', 'руководитель информационной безопасности'], 'information_security'),
42
- sourceRole('architect', 'Architect', ['arxitektor loyihachi', 'arxitektor'], 'construction'),
43
- sourceRole('economist', 'Economist', ['iqtisodchi', 'iqtsodchi', 'iqtisodiy'], 'finance'),
44
- sourceRole('logistics_manager', 'Logistics Specialist', ['logist', 'logistics specialist', 'логист'], 'logistics'),
45
- sourceRole('frontend_developer', 'Frontend Developer', ['frontet', 'frontet developer', 'frontet dasturchi'], 'software_development'),
46
- sourceRole('nanny', 'Nanny', ['bolalarga qarash', 'bolaga qarash'], 'education'),
47
- sourceRole('loader', 'Loader', ['грузчиком', 'грузчика', 'грузчик'], 'logistics'),
48
- sourceRole('welder', 'Welder', ['сварщиком', 'сварщица', 'зварювальником'], 'construction'),
49
- ]);
50
-
51
- const sourceRoleNormalization = (canonical, label, re) => Object.freeze({ canonical, label, re });
52
-
53
- /**
54
- * Anchored or strongly contextualized raw-role repairs. These rules are kept
55
- * separate from SOURCE_PROFESSION_ALIASES so generic values such as `model`,
56
- * `online`, or `bank` never become profession matches in arbitrary CV prose.
57
- */
58
- export const SOURCE_ROLE_NORMALIZATION_RULES = Object.freeze([
59
- sourceRoleNormalization('economist', 'Economist', /^iqt(?:i)?sodchi$/iu),
60
- sourceRoleNormalization('economist', 'Economist', /^iqtisodiy$/iu),
61
- sourceRoleNormalization('logistics_manager', 'Logistics Specialist', /^logist(?:ika)?(?:\s+updater)?$/iu),
62
- sourceRoleNormalization('english_teacher', 'English Teacher', /^ingliz\s+tili\s+ustoz(?:iman)?$/iu),
63
- sourceRoleNormalization('frontend_developer', 'Frontend Developer', /mobilagraf[\s\S]*itishnik[\s\S]*front(?:et|ent|end)/iu),
64
- sourceRoleNormalization('nanny', 'Nanny', /farqi\s+yo[\s\S]*bolalarga\s+qarash/iu),
65
- sourceRoleNormalization('sales_manager', 'Sales Manager', /^(?:sales\s+executive(?:\s+ind)?|роп(?:,?\s*sales\s+executive)?)$/iu),
66
- sourceRoleNormalization('operative_officer', 'Operative Officer', /^(?:оперативник|оперуполномоченн\p{L}*|оперативный\s+уполномоченн\p{L}*)$/iu),
67
- sourceRoleNormalization('water_supply_specialist', 'Water Supply Specialist', /^(?:suv\s+ta['’ʻʼ‘`]?minoti|водоснабжение)$/iu),
68
- sourceRoleNormalization('any_role', 'Any Role', /^(?:onlayn(?:\s+ish(?:chi)?)?|online(?:\s+ish(?:chi)?)?|онлайн|удал[её]нно|remote(?:\s+work)?|boshqa\s+ishlar?|farqi\s+(?:yo['’ʻʼ‘`]?q|yuq)|tungi|bilmaym\p{L}*)$/iu),
69
- sourceRoleNormalization('restaurant_cafe_worker', 'Restaurant / Cafe Worker', /^ищу\s+работу\s+(?:в\s+)?(?:кафе|ресторанах?|кафе\s+или\s+ресторанах)$/iu),
70
- sourceRoleNormalization('driver', 'Driver', /^(?:xaydovchilik|haydovchilik|shafyorlik|shofyorlik)/iu),
71
- sourceRoleNormalization('retail_worker', 'Retail Worker', /^do['’ʻʼ‘`]?kon$/iu),
72
- sourceRoleNormalization('salesperson', 'Salesperson', /^(?:savdo|sotuvchi)$/iu),
73
- sourceRoleNormalization('pharmacist', 'Pharmacist', /^(?:dorishunos|farmatsevt)$/iu),
74
- sourceRoleNormalization('notary_assistant', 'Notary Assistant', /^(?:natarus|notarius)\s+yordamchisi/iu),
75
- sourceRoleNormalization('librarian', 'Librarian', /^kutubxonachi$/iu),
76
- sourceRoleNormalization('singer_vocalist', 'Singer / Vocalist', /^(?:vokal\s*:\s*)?xonanda$/iu),
77
- sourceRoleNormalization('model', 'Model', /^model$/iu),
78
- sourceRoleNormalization('flight_attendant', 'Flight Attendant', /^bortprovodnik$|^бортпроводник$/iu),
79
- sourceRoleNormalization('hvac_technician', 'HVAC Technician', /^(?:konditsaner|kanditsaner|konditsioner)/iu),
80
- sourceRoleNormalization('mobile_content_creator', 'Mobile Content Creator', /^mobilografiya(?:\s+bo['’ʻʼ‘`]?yicha)?$/iu),
81
- sourceRoleNormalization('cctv_intercom_technician', 'CCTV / Intercom Technician', /kamera\s+(?:dama?fon|domofon)|domofon\s+xizmat/iu),
82
- sourceRoleNormalization('internal_control_specialist', 'Internal Control Specialist', /^ichki\s+nazoratchi$/iu),
83
- sourceRoleNormalization('brand_ambassador', 'Brand Ambassador', /^(?:бренд\s+фейс|brand\s+face)$/iu),
84
- sourceRoleNormalization('insurance_specialist', 'Insurance Specialist', /^sug['’ʻʼ‘`]?urta$/iu),
85
- sourceRoleNormalization('bank_operations_specialist', 'Bank Operations Specialist', /^стаж[её]р\s+операционист|^операционист$/iu),
86
- sourceRoleNormalization('commercial_director', 'Commercial Director', /^коммерческ\p{L}*\s+директор|\bchief\s+commercial\s+officer\b|\bCCO\b/iu),
87
- sourceRoleNormalization('security_specialist', 'Security Specialist', /^по\s+безопасност\p{L}*\s+объекта$/iu),
88
- sourceRoleNormalization('healthcare_specialist', 'Healthcare Specialist', /^mededsina$|^meditsina$|^медицина$/iu),
89
- sourceRoleNormalization('tourism_hospitality_specialist', 'Tourism / Hospitality Specialist', /^mehmonxona[^\n]*turfirma|^turfirma[^\n]*mehmonxona/iu),
90
- sourceRoleNormalization('confectioner', 'Confectioner', /qandolat|qandolatchi/iu),
91
- ]);
92
-
93
- export function normalizeSourceRole(value) {
94
- const text = String(value || '').trim();
95
- if (!text) return null;
96
- const match = SOURCE_ROLE_NORMALIZATION_RULES.find((entry) => entry.re.test(text));
97
- return match ? Object.freeze({ canonical: match.canonical, label: match.label }) : null;
98
- }
99
-
100
- export const SOURCE_CANDIDATE_INTENT_ALIASES = Object.freeze([
101
- 'работу ищу',
102
- 'роботу шукаю',
103
- 'срочно работу ищу',
104
- 'терміново роботу шукаю',
105
- 'могу работать',
106
- 'можу працювати',
107
- ]);
108
-
109
- const SOURCE_CANDIDATE_INTENT_RE = aliasesToRegex(SOURCE_CANDIDATE_INTENT_ALIASES);
110
-
111
- export function matchesSourceCandidateIntent(value) {
112
- return SOURCE_CANDIDATE_INTENT_RE.test(String(value || ''));
113
- }
114
-
115
- const LABELS = new Map(SOURCE_PROFESSION_ALIASES.map((entry) => [entry.canonical, entry.label]));
116
- const ACRONYMS = new Map([
117
- ['qa', 'QA'], ['hr', 'HR'], ['ui', 'UI'], ['ux', 'UX'], ['ai', 'AI'], ['ml', 'ML'],
118
- ['seo', 'SEO'], ['sre', 'SRE'], ['dba', 'DBA'], ['crm', 'CRM'], ['erp', 'ERP'], ['pmo', 'PMO'], ['it', 'IT'],
119
- ]);
120
-
121
- export function professionDisplayLabel(canonical) {
122
- if (!canonical) return '';
123
- const explicit = LABELS.get(canonical);
124
- if (explicit) return explicit;
125
- return String(canonical).split('_').map((part) => ACRONYMS.get(part) || `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(' ');
126
- }
127
-
128
- function sourceMatches(text) {
129
- const matches = [];
130
- for (const entry of SOURCE_PROFESSION_ALIASES) {
131
- const match = String(text || '').match(entry.re);
132
- if (!match) continue;
133
- matches.push({
134
- canonical: entry.canonical,
135
- group: entry.group,
136
- family: entry.group,
137
- score: 1,
138
- strength: 'strong',
139
- matched: match[0].trim(),
140
- index: match.index ?? 0,
141
- label: entry.label,
142
- });
143
- }
144
- return matches;
145
- }
146
-
147
- /**
148
- * Combine the core taxonomy with real-source aliases. More specific matches
149
- * suppress contained generic matches ("Chief Accountant" must not also become
150
- * "Accountant"), while separate spans remain separate professions.
151
- */
152
- export function matchExtendedProfessions(value, { limit = 5, allowWeak = true } = {}) {
153
- const text = String(value || '');
154
- if (!text) return [];
155
- const base = matchProfessions(text, { limit: Math.max(limit * 4, 24), allowWeak }).map((match) => {
156
- const index = text.toLocaleLowerCase().indexOf(String(match.matched || '').toLocaleLowerCase());
157
- return { ...match, index: index < 0 ? 0 : index, label: professionDisplayLabel(match.canonical) };
158
- });
159
- const matches = [...base, ...sourceMatches(text)]
160
- .sort((a, b) => b.score - a.score || b.matched.length - a.matched.length || a.index - b.index);
161
-
162
- const selected = [];
163
- const canonicals = new Set();
164
- for (const match of matches) {
165
- if (canonicals.has(match.canonical)) continue;
166
- const start = match.index;
167
- const end = start + match.matched.length;
168
- const contained = selected.some((chosen) => {
169
- const chosenStart = chosen.index;
170
- const chosenEnd = chosenStart + chosen.matched.length;
171
- return chosen.score >= match.score && chosenStart <= start && chosenEnd >= end && chosen.matched.length > match.matched.length;
172
- });
173
- if (contained) continue;
174
- canonicals.add(match.canonical);
175
- selected.push({ ...match });
176
- }
177
-
178
- // A concrete software role outranks a generic "IT specialist" mention in
179
- // the same role/title text, even when the two aliases occupy separate spans.
180
- const hasSoftwareRole = selected.some((match) => match.group === 'software_development');
181
- const filtered = hasSoftwareRole
182
- ? selected.filter((match) => match.canonical !== 'it_specialist')
183
- : selected;
184
-
185
- return Object.freeze(filtered.slice(0, limit).map((match) => Object.freeze(match)));
1
+ import { aliasesToRegex } from './normalization.js';
2
+ import { matchProfessions } from './hiring-professions.js';
3
+
4
+ const sourceRole = (canonical, label, aliases, group = 'other') => Object.freeze({
5
+ canonical,
6
+ label,
7
+ group,
8
+ aliases: Object.freeze([...new Set(aliases)]),
9
+ re: aliasesToRegex([...new Set(aliases)]),
10
+ });
11
+
12
+ /**
13
+ * Source spellings seen in real CV feeds which are too source-specific or
14
+ * colloquial for the core profession rows. They still live in the shared
15
+ * package so consumers do not grow their own profession regex catalogs.
16
+ */
17
+ export const SOURCE_PROFESSION_ALIASES = Object.freeze([
18
+ sourceRole('chief_executive_officer', 'Chief Executive Officer', ['ceo', 'chief executive officer', 'генеральный директор', 'гендиректор', 'виконавчий директор'], 'management'),
19
+ sourceRole('chief_technology_officer', 'Chief Technology Officer', ['cto', 'chief technology officer', 'технический директор', 'технічний директор'], 'management'),
20
+ sourceRole('sales_manager', 'Sales Manager', ['sales executive', 'head of sales', 'менеджер экспортных продаж', 'менеджер по экспортным продажам', 'роп', 'sotuv menejer'], 'sales'),
21
+ sourceRole('driver', 'Driver', ['се категория', 'се категория буйича', 'ce категория', 'ce category', 'shafyor'], 'transport'),
22
+ sourceRole('accountant', 'Accountant', ['bugalteriya', 'buxgalteriya', 'buhgalteriya'], 'finance'),
23
+ sourceRole('cashier', 'Cashier', ['kassa xodimi', 'kassa mudiri'], 'retail'),
24
+ sourceRole('notary', 'Notary', ['notary', 'notarius', 'нотариус'], 'legal'),
25
+ sourceRole('metrology_specialist', 'Metrology Specialist', ['metrologiya', 'metrology specialist', 'метролог', 'standartlashtirish'], 'manufacturing'),
26
+ sourceRole('security_guard', 'Security Guard', ['xavfsizlik', 'qoriqlash', 'qo‘riqlash', "qo'riqlash", 'охорона'], 'security'),
27
+ sourceRole('finance_banking_specialist', 'Finance / Banking Specialist', ['finance specialist', 'banking specialist', 'moliya', 'soliq', 'bank'], 'finance'),
28
+ sourceRole('teacher', 'Teacher', ['tyutorlik', 'тьютор', 'titur', "o'qituvchilik", 'o‘qituvchilik', 'oʻqituvchilik', 'ustoz'], 'education'),
29
+ sourceRole('english_teacher', 'English Teacher', ['ingliz tili ustoziman', 'ingliz tili ustoz', 'ingliz tili oqituvchi', "ingliz tili o'qituvchi"], 'education'),
30
+ sourceRole('it_specialist', 'IT Specialist', ['it specialist', 'itishnik', 'it ishnik', 'kompyuter boyicha ish', "kompyuter bo'yicha ish", 'kompyuter bo‘yicha ish'], 'infrastructure'),
31
+ sourceRole('biotechnologist', 'Biotechnologist', ['biotechnologist', 'биотехнолог', 'biotexnolog'], 'medicine'),
32
+ sourceRole('laboratory_technician', 'Laboratory Technician', ['laborant'], 'medicine'),
33
+ sourceRole('media_specialist', 'Media Specialist', ['media specialist', 'специалист по сми', 'matbuot'], 'media_content'),
34
+ sourceRole('engineer', 'Engineer', ['engineer', 'инженер', 'інженер', 'muhandis', 'injiner'], 'engineering'),
35
+ sourceRole('chat_operator', 'Chat Operator', ['chat operatori'], 'customer_support'),
36
+ sourceRole('call_center_operator', 'Call Center Operator', ['koll-markaz operatori', 'koll markaz operatori', 'call-markaz operatori'], 'customer_support'),
37
+ sourceRole('consultant', 'Consultant', ['consultant', 'консультант', 'консультантка'], 'commercial'),
38
+ sourceRole('supervisor', 'Supervisor', ['supervisor', 'супервайзер', 'начальник отряд', 'начальник отряда'], 'management'),
39
+ sourceRole('quality_inspector', 'Quality Inspector', ['quality inspector', 'инспектор по качеству', 'інспектор з якості'], 'manufacturing'),
40
+ sourceRole('oil_gas_worker', 'Oil & Gas Worker', ['oil and gas', 'oil & gas', 'нефть и газ', 'нефтегаз', 'neft va gaz', 'neft vagaz', 'neft vagaz sohasida', 'neft va gaz sohasida'], 'manufacturing'),
41
+ sourceRole('cybersecurity_specialist', 'Cybersecurity Specialist', ['cybersecurity specialist', 'ciso', 'руководитель по информационной безопасности', 'руководитель информационной безопасности'], 'information_security'),
42
+ sourceRole('architect', 'Architect', ['arxitektor loyihachi', 'arxitektor'], 'construction'),
43
+ sourceRole('economist', 'Economist', ['iqtisodchi', 'iqtsodchi', 'iqtisodiy'], 'finance'),
44
+ sourceRole('logistics_manager', 'Logistics Specialist', ['logist', 'logistics specialist', 'логист'], 'logistics'),
45
+ sourceRole('frontend_developer', 'Frontend Developer', ['frontet', 'frontet developer', 'frontet dasturchi'], 'software_development'),
46
+ sourceRole('nanny', 'Nanny', ['bolalarga qarash', 'bolaga qarash'], 'education'),
47
+ sourceRole('loader', 'Loader', ['грузчиком', 'грузчика', 'грузчик'], 'logistics'),
48
+ sourceRole('welder', 'Welder', ['сварщиком', 'сварщица', 'зварювальником'], 'construction'),
49
+ ]);
50
+
51
+ const sourceRoleNormalization = (canonical, label, re) => Object.freeze({ canonical, label, re });
52
+
53
+ /**
54
+ * Anchored or strongly contextualized raw-role repairs. These rules are kept
55
+ * separate from SOURCE_PROFESSION_ALIASES so generic values such as `model`,
56
+ * `online`, or `bank` never become profession matches in arbitrary CV prose.
57
+ */
58
+ export const SOURCE_ROLE_NORMALIZATION_RULES = Object.freeze([
59
+ sourceRoleNormalization('economist', 'Economist', /^iqt(?:i)?sodchi$/iu),
60
+ sourceRoleNormalization('economist', 'Economist', /^iqtisodiy$/iu),
61
+ sourceRoleNormalization('logistics_manager', 'Logistics Specialist', /^logist(?:ika)?(?:\s+updater)?$/iu),
62
+ sourceRoleNormalization('english_teacher', 'English Teacher', /^ingliz\s+tili\s+ustoz(?:iman)?$/iu),
63
+ sourceRoleNormalization('frontend_developer', 'Frontend Developer', /mobilagraf[\s\S]*itishnik[\s\S]*front(?:et|ent|end)/iu),
64
+ sourceRoleNormalization('nanny', 'Nanny', /farqi\s+yo[\s\S]*bolalarga\s+qarash/iu),
65
+ sourceRoleNormalization('sales_manager', 'Sales Manager', /^(?:sales\s+executive(?:\s+ind)?|роп(?:,?\s*sales\s+executive)?)$/iu),
66
+ sourceRoleNormalization('operative_officer', 'Operative Officer', /^(?:оперативник|оперуполномоченн\p{L}*|оперативный\s+уполномоченн\p{L}*)$/iu),
67
+ sourceRoleNormalization('water_supply_specialist', 'Water Supply Specialist', /^(?:suv\s+ta['’ʻʼ‘`]?minoti|водоснабжение)$/iu),
68
+ sourceRoleNormalization('any_role', 'Any Role', /^(?:onlayn(?:\s+ish(?:chi)?)?|online(?:\s+ish(?:chi)?)?|онлайн|удал[её]нно|remote(?:\s+work)?|boshqa\s+ishlar?|farqi\s+(?:yo['’ʻʼ‘`]?q|yuq)|tungi|bilmaym\p{L}*)$/iu),
69
+ sourceRoleNormalization('restaurant_cafe_worker', 'Restaurant / Cafe Worker', /^ищу\s+работу\s+(?:в\s+)?(?:кафе|ресторанах?|кафе\s+или\s+ресторанах)$/iu),
70
+ sourceRoleNormalization('driver', 'Driver', /^(?:xaydovchilik|haydovchilik|shafyorlik|shofyorlik)/iu),
71
+ sourceRoleNormalization('retail_worker', 'Retail Worker', /^do['’ʻʼ‘`]?kon$/iu),
72
+ sourceRoleNormalization('salesperson', 'Salesperson', /^(?:savdo|sotuvchi)$/iu),
73
+ sourceRoleNormalization('pharmacist', 'Pharmacist', /^(?:dorishunos|farmatsevt)$/iu),
74
+ sourceRoleNormalization('notary_assistant', 'Notary Assistant', /^(?:natarus|notarius)\s+yordamchisi/iu),
75
+ sourceRoleNormalization('librarian', 'Librarian', /^kutubxonachi$/iu),
76
+ sourceRoleNormalization('singer_vocalist', 'Singer / Vocalist', /^(?:vokal\s*:\s*)?xonanda$/iu),
77
+ sourceRoleNormalization('model', 'Model', /^model$/iu),
78
+ sourceRoleNormalization('flight_attendant', 'Flight Attendant', /^bortprovodnik$|^бортпроводник$/iu),
79
+ sourceRoleNormalization('hvac_technician', 'HVAC Technician', /^(?:konditsaner|kanditsaner|konditsioner)/iu),
80
+ sourceRoleNormalization('mobile_content_creator', 'Mobile Content Creator', /^mobilografiya(?:\s+bo['’ʻʼ‘`]?yicha)?$/iu),
81
+ sourceRoleNormalization('cctv_intercom_technician', 'CCTV / Intercom Technician', /kamera\s+(?:dama?fon|domofon)|domofon\s+xizmat/iu),
82
+ sourceRoleNormalization('internal_control_specialist', 'Internal Control Specialist', /^ichki\s+nazoratchi$/iu),
83
+ sourceRoleNormalization('brand_ambassador', 'Brand Ambassador', /^(?:бренд\s+фейс|brand\s+face)$/iu),
84
+ sourceRoleNormalization('insurance_specialist', 'Insurance Specialist', /^sug['’ʻʼ‘`]?urta$/iu),
85
+ sourceRoleNormalization('bank_operations_specialist', 'Bank Operations Specialist', /^стаж[её]р\s+операционист|^операционист$/iu),
86
+ sourceRoleNormalization('commercial_director', 'Commercial Director', /^коммерческ\p{L}*\s+директор|\bchief\s+commercial\s+officer\b|\bCCO\b/iu),
87
+ sourceRoleNormalization('security_specialist', 'Security Specialist', /^по\s+безопасност\p{L}*\s+объекта$/iu),
88
+ sourceRoleNormalization('healthcare_specialist', 'Healthcare Specialist', /^mededsina$|^meditsina$|^медицина$/iu),
89
+ sourceRoleNormalization('tourism_hospitality_specialist', 'Tourism / Hospitality Specialist', /^mehmonxona[^\n]*turfirma|^turfirma[^\n]*mehmonxona/iu),
90
+ sourceRoleNormalization('confectioner', 'Confectioner', /qandolat|qandolatchi/iu),
91
+ ]);
92
+
93
+ export function normalizeSourceRole(value) {
94
+ const text = String(value || '').trim();
95
+ if (!text) return null;
96
+ const match = SOURCE_ROLE_NORMALIZATION_RULES.find((entry) => entry.re.test(text));
97
+ return match ? Object.freeze({ canonical: match.canonical, label: match.label }) : null;
98
+ }
99
+
100
+ export const SOURCE_CANDIDATE_INTENT_ALIASES = Object.freeze([
101
+ 'работу ищу',
102
+ 'роботу шукаю',
103
+ 'срочно работу ищу',
104
+ 'терміново роботу шукаю',
105
+ 'могу работать',
106
+ 'можу працювати',
107
+ ]);
108
+
109
+ const SOURCE_CANDIDATE_INTENT_RE = aliasesToRegex(SOURCE_CANDIDATE_INTENT_ALIASES);
110
+
111
+ export function matchesSourceCandidateIntent(value) {
112
+ return SOURCE_CANDIDATE_INTENT_RE.test(String(value || ''));
113
+ }
114
+
115
+ const LABELS = new Map(SOURCE_PROFESSION_ALIASES.map((entry) => [entry.canonical, entry.label]));
116
+ const ACRONYMS = new Map([
117
+ ['qa', 'QA'], ['hr', 'HR'], ['ui', 'UI'], ['ux', 'UX'], ['ai', 'AI'], ['ml', 'ML'],
118
+ ['seo', 'SEO'], ['sre', 'SRE'], ['dba', 'DBA'], ['crm', 'CRM'], ['erp', 'ERP'], ['pmo', 'PMO'], ['it', 'IT'],
119
+ ]);
120
+
121
+ export function professionDisplayLabel(canonical) {
122
+ if (!canonical) return '';
123
+ const explicit = LABELS.get(canonical);
124
+ if (explicit) return explicit;
125
+ return String(canonical).split('_').map((part) => ACRONYMS.get(part) || `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(' ');
126
+ }
127
+
128
+ function sourceMatches(text) {
129
+ const matches = [];
130
+ for (const entry of SOURCE_PROFESSION_ALIASES) {
131
+ const match = String(text || '').match(entry.re);
132
+ if (!match) continue;
133
+ matches.push({
134
+ canonical: entry.canonical,
135
+ group: entry.group,
136
+ family: entry.group,
137
+ score: 1,
138
+ strength: 'strong',
139
+ matched: match[0].trim(),
140
+ index: match.index ?? 0,
141
+ label: entry.label,
142
+ });
143
+ }
144
+ return matches;
145
+ }
146
+
147
+ /**
148
+ * Combine the core taxonomy with real-source aliases. More specific matches
149
+ * suppress contained generic matches ("Chief Accountant" must not also become
150
+ * "Accountant"), while separate spans remain separate professions.
151
+ */
152
+ export function matchExtendedProfessions(value, { limit = 5, allowWeak = true } = {}) {
153
+ const text = String(value || '');
154
+ if (!text) return [];
155
+ const base = matchProfessions(text, { limit: Math.max(limit * 4, 24), allowWeak }).map((match) => {
156
+ const index = text.toLocaleLowerCase().indexOf(String(match.matched || '').toLocaleLowerCase());
157
+ return { ...match, index: index < 0 ? 0 : index, label: professionDisplayLabel(match.canonical) };
158
+ });
159
+ const matches = [...base, ...sourceMatches(text)]
160
+ .sort((a, b) => b.score - a.score || b.matched.length - a.matched.length || a.index - b.index);
161
+
162
+ const selected = [];
163
+ const canonicals = new Set();
164
+ for (const match of matches) {
165
+ if (canonicals.has(match.canonical)) continue;
166
+ const start = match.index;
167
+ const end = start + match.matched.length;
168
+ const contained = selected.some((chosen) => {
169
+ const chosenStart = chosen.index;
170
+ const chosenEnd = chosenStart + chosen.matched.length;
171
+ return chosen.score >= match.score && chosenStart <= start && chosenEnd >= end && chosen.matched.length > match.matched.length;
172
+ });
173
+ if (contained) continue;
174
+ canonicals.add(match.canonical);
175
+ selected.push({ ...match });
176
+ }
177
+
178
+ // A concrete software role outranks a generic "IT specialist" mention in
179
+ // the same role/title text, even when the two aliases occupy separate spans.
180
+ const hasSoftwareRole = selected.some((match) => match.group === 'software_development');
181
+ const filtered = hasSoftwareRole
182
+ ? selected.filter((match) => match.canonical !== 'it_specialist')
183
+ : selected;
184
+
185
+ return Object.freeze(filtered.slice(0, limit).map((match) => Object.freeze(match)));
186
186
  }
@@ -1,31 +1,31 @@
1
- import type { SalaryParseResult } from '../index.d.ts';
2
-
3
- export type CandidateExperienceMention = Readonly<{ years: number; context: string; approximate?: true }>;
4
- export type VisaSponsorshipWording = 'offered' | 'notOffered' | null;
5
-
6
- export function extractCandidateStructuredField(value: unknown, key: string, maxLength?: number): string | null;
7
- export function extractJobStructuredField(value: unknown, key: string, maxLength?: number): string | null;
8
- export function extractCandidateDisplayName(value: unknown): string;
9
- export function extractCandidateExperienceMentions(value: unknown): readonly CandidateExperienceMention[];
10
- export function parseHiringSourceSalary(value: unknown): SalaryParseResult | null;
11
- export function parseCandidateSalary(value: unknown, country?: string): SalaryParseResult | null;
12
- export function detectUsLocation(value: unknown): boolean;
13
- export function detectVisaSponsorshipWording(value: unknown): VisaSponsorshipWording;
14
- export const TEMPORARY_WORK_AUTH_RE: RegExp;
15
- export function detectRecruitmentAgency(value: unknown): boolean;
16
- export function extractNiceToHaveContext(value: unknown, maxLength?: number): string;
17
- export type CandidatePostSignals = Readonly<{
18
- candidateForm: boolean;
19
- cvMarker: boolean;
20
- firstPerson: boolean;
21
- personalProfile: boolean;
22
- contact: boolean;
23
- emptyRecommendation: boolean;
24
- sectionCount: number;
25
- }>;
26
- export function detectCandidatePostSignals(value: unknown): CandidatePostSignals;
27
- export function extractCandidateStructuredBlock(value: unknown, key: string, maxLength?: number): string | null;
28
- export function defaultHiringCurrency(country: unknown): string | null;
29
- export function isHiringCharityAppeal(value: unknown): boolean;
30
- export function isHiringRecruitingOpportunity(value: unknown): boolean;
31
- export function sameHiringProfessionFamily(a: unknown, b: unknown): boolean;
1
+ import type { SalaryParseResult } from '../index.d.ts';
2
+
3
+ export type CandidateExperienceMention = Readonly<{ years: number; context: string; approximate?: true }>;
4
+ export type VisaSponsorshipWording = 'offered' | 'notOffered' | null;
5
+
6
+ export function extractCandidateStructuredField(value: unknown, key: string, maxLength?: number): string | null;
7
+ export function extractJobStructuredField(value: unknown, key: string, maxLength?: number): string | null;
8
+ export function extractCandidateDisplayName(value: unknown): string;
9
+ export function extractCandidateExperienceMentions(value: unknown): readonly CandidateExperienceMention[];
10
+ export function parseHiringSourceSalary(value: unknown): SalaryParseResult | null;
11
+ export function parseCandidateSalary(value: unknown, country?: string): SalaryParseResult | null;
12
+ export function detectUsLocation(value: unknown): boolean;
13
+ export function detectVisaSponsorshipWording(value: unknown): VisaSponsorshipWording;
14
+ export const TEMPORARY_WORK_AUTH_RE: RegExp;
15
+ export function detectRecruitmentAgency(value: unknown): boolean;
16
+ export function extractNiceToHaveContext(value: unknown, maxLength?: number): string;
17
+ export type CandidatePostSignals = Readonly<{
18
+ candidateForm: boolean;
19
+ cvMarker: boolean;
20
+ firstPerson: boolean;
21
+ personalProfile: boolean;
22
+ contact: boolean;
23
+ emptyRecommendation: boolean;
24
+ sectionCount: number;
25
+ }>;
26
+ export function detectCandidatePostSignals(value: unknown): CandidatePostSignals;
27
+ export function extractCandidateStructuredBlock(value: unknown, key: string, maxLength?: number): string | null;
28
+ export function defaultHiringCurrency(country: unknown): string | null;
29
+ export function isHiringCharityAppeal(value: unknown): boolean;
30
+ export function isHiringRecruitingOpportunity(value: unknown): boolean;
31
+ export function sameHiringProfessionFamily(a: unknown, b: unknown): boolean;