salesprompter-cli 0.1.20 → 0.1.23

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.
@@ -0,0 +1,454 @@
1
+ import { classifyDirectPathSegment } from "./direct-path.js";
2
+ import { parseHistoricalQuery } from "./historical-queries.js";
3
+ function marketCountries(market) {
4
+ if (market === "dach") {
5
+ return ["DE", "AT", "CH"];
6
+ }
7
+ if (market === "europe") {
8
+ return ["DE", "AT", "CH", "NL", "GB", "FR", "SE", "DK"];
9
+ }
10
+ return [];
11
+ }
12
+ function sqlStringList(values) {
13
+ return values.map((value) => `'${value.replaceAll("'", "\\'")}'`).join(", ");
14
+ }
15
+ function broadTitlePredicateSql(field) {
16
+ return [
17
+ `LOWER(${field}) LIKE '%head of hr%'`,
18
+ `LOWER(${field}) LIKE '%head of human resources%'`,
19
+ `LOWER(${field}) LIKE '%head of people%'`,
20
+ `LOWER(${field}) LIKE '%head of payroll%'`,
21
+ `LOWER(${field}) LIKE '%people services%'`,
22
+ `LOWER(${field}) LIKE '%shared services%'`,
23
+ `LOWER(${field}) LIKE '%total rewards%'`,
24
+ `LOWER(${field}) LIKE '%compensation%'`,
25
+ `LOWER(${field}) LIKE '%benefits%'`,
26
+ `LOWER(${field}) LIKE '%hr business partner%'`,
27
+ `LOWER(${field}) LIKE '%hrbp%'`,
28
+ `LOWER(${field}) LIKE '%people operations%'`,
29
+ `LOWER(${field}) LIKE '%people ops%'`,
30
+ `LOWER(${field}) LIKE '%hr operations%'`,
31
+ `LOWER(${field}) LIKE '%hr administration%'`,
32
+ `LOWER(${field}) LIKE '%hr director%'`,
33
+ `LOWER(${field}) LIKE '%director hr%'`,
34
+ `LOWER(${field}) LIKE '%chief people officer%'`,
35
+ `LOWER(${field}) LIKE '%chro%'`,
36
+ `LOWER(${field}) LIKE '%leiter personal%'`,
37
+ `LOWER(${field}) LIKE '%leiter der personalabteilung%'`,
38
+ `LOWER(${field}) LIKE '%personalleitung%'`,
39
+ `LOWER(${field}) LIKE '%bereichsleiter personal%'`,
40
+ `LOWER(${field}) LIKE '%geschäftsbereichsleiter personal%'`,
41
+ `LOWER(${field}) LIKE '%geschaeftsbereichsleiter personal%'`
42
+ ].join("\n OR ");
43
+ }
44
+ function titleExclusionPredicateSql(field) {
45
+ return [
46
+ `LOWER(${field}) NOT LIKE '%assistant%'`,
47
+ `LOWER(${field}) NOT LIKE '%assistent%'`,
48
+ `LOWER(${field}) NOT LIKE '%assistentin%'`,
49
+ `LOWER(${field}) NOT LIKE '%recruitment%'`,
50
+ `LOWER(${field}) NOT LIKE '%recruiter%'`,
51
+ `LOWER(${field}) NOT LIKE '%talent acquisition specialist%'`
52
+ ].join("\n AND ");
53
+ }
54
+ export function buildDeelOutreachExportSql(options) {
55
+ const countries = marketCountries(options.market);
56
+ const countryClause = countries.length > 0 ? `UPPER(CAST(p.company_countryCode AS STRING)) IN (${sqlStringList(countries)})` : "TRUE";
57
+ return `WITH hr_leadlists AS (
58
+ SELECT
59
+ CAST(leadListId AS STRING) AS leadListId,
60
+ CAST(query AS STRING) AS query,
61
+ leadList_container_ts
62
+ FROM \`icpidentifier.SalesPrompter.leadLists_raw\`
63
+ WHERE query IS NOT NULL
64
+ AND LOWER(CAST(query AS STRING)) LIKE '%/sales/search/people%'
65
+ AND REGEXP_CONTAINS(LOWER(CAST(query AS STRING)), r'type%3afunction[^)]*id%3a12[^)]*selectiontype%3aincluded')
66
+ AND NOT REGEXP_CONTAINS(LOWER(CAST(query AS STRING)), r'id%3a12[^)]*selectiontype%3aexcluded')
67
+ QUALIFY ROW_NUMBER() OVER (
68
+ PARTITION BY CAST(leadListId AS STRING)
69
+ ORDER BY leadList_container_ts DESC
70
+ ) = 1
71
+ )
72
+ SELECT
73
+ CAST(p.leadListId AS STRING) AS leadListId,
74
+ CAST(p.contactId AS STRING) AS contactId,
75
+ CAST(p.linkedin_contacts_companyId AS STRING) AS companyId,
76
+ CAST(p.firstName_cleaned AS STRING) AS firstName,
77
+ CAST(p.lastName_cleaned AS STRING) AS lastName,
78
+ CONCAT(
79
+ COALESCE(CAST(p.firstName_cleaned AS STRING), ''),
80
+ IF(p.firstName_cleaned IS NOT NULL AND p.lastName_cleaned IS NOT NULL, ' ', ''),
81
+ COALESCE(CAST(p.lastName_cleaned AS STRING), '')
82
+ ) AS fullName,
83
+ CAST(p.jobTitle AS STRING) AS jobTitle,
84
+ CAST(p.companyName AS STRING) AS companyName,
85
+ CAST(p.linkedin_companies_handle AS STRING) AS linkedin_companies_handle,
86
+ CONCAT('https://www.linkedin.com/company/', CAST(p.linkedin_contacts_companyId AS STRING)) AS companyUrl,
87
+ CAST(p.companySize AS STRING) AS companySize,
88
+ CAST(p.company_countryCode AS STRING) AS countryCode,
89
+ CAST(p.headquarters AS STRING) AS headquarters,
90
+ CAST(p.domain AS STRING) AS domain,
91
+ CAST(p.domain_linkedin AS STRING) AS domain_linkedin,
92
+ CAST(p.industry AS STRING) AS industry,
93
+ CAST(p.contact_location AS STRING) AS contact_location,
94
+ CAST(p.contact_companyLocation AS STRING) AS contact_companyLocation,
95
+ SAFE_CAST(p.tenureAtCompany AS FLOAT64) AS tenureAtCompany,
96
+ SAFE_CAST(p.tenureAtPosition AS FLOAT64) AS tenureAtPosition,
97
+ CAST(p.contact_summary AS STRING) AS contact_summary,
98
+ CAST(p.contact_titleDescription AS STRING) AS contact_titleDescription,
99
+ CAST(p.leadList_container_ts AS STRING) AS leadList_container_ts,
100
+ CAST(p.timestamp AS STRING) AS company_ts,
101
+ l.query AS leadListQuery,
102
+ CAST(p.email AS STRING) AS email,
103
+ SAFE_CAST(p.emailScore AS INT64) AS emailScore,
104
+ SAFE_CAST(p.gender AS INT64) AS gender,
105
+ COALESCE(p.emails_accept_all, FALSE) AS emails_accept_all
106
+ FROM \`icpidentifier.SalesGPT.leadPool_new\` p
107
+ JOIN hr_leadlists l ON CAST(p.leadListId AS STRING) = l.leadListId
108
+ WHERE CAST(p.functionId AS STRING) = '12'
109
+ AND ${countryClause}
110
+ AND CAST(p.email AS STRING) IS NOT NULL
111
+ AND COALESCE(p.email_invalid, FALSE) = FALSE
112
+ AND COALESCE(p.email_bounced, FALSE) = FALSE
113
+ AND COALESCE(p.company_blacklisted, FALSE) = FALSE
114
+ AND COALESCE(p.company_inSequence, FALSE) = FALSE
115
+ AND COALESCE(p.contact_replied, FALSE) = FALSE
116
+ AND COALESCE(p.contact_bounced, FALSE) = FALSE
117
+ AND COALESCE(p.jobTitle_blacklisted, FALSE) = FALSE
118
+ AND COALESCE(p.domainBlacklisted, FALSE) = FALSE
119
+ AND CAST(p.domain AS STRING) IS NOT NULL
120
+ AND CAST(p.firstName_cleaned AS STRING) IS NOT NULL
121
+ AND CAST(p.lastName_cleaned AS STRING) IS NOT NULL
122
+ AND SAFE_CAST(p.emailScore AS INT64) >= ${Math.trunc(options.minEmailScore)}
123
+ AND CAST(p.companySize AS STRING) IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
124
+ AND (
125
+ ${broadTitlePredicateSql("CAST(p.jobTitle AS STRING)")}
126
+ )
127
+ AND ${titleExclusionPredicateSql("CAST(p.jobTitle AS STRING)")}
128
+ QUALIFY ROW_NUMBER() OVER (
129
+ PARTITION BY LOWER(CAST(p.email AS STRING))
130
+ ORDER BY SAFE_CAST(p.emailScore AS INT64) DESC NULLS LAST, p.timestamp DESC, p.contact_ts DESC
131
+ ) = 1
132
+ ORDER BY SAFE_CAST(p.emailScore AS INT64) DESC NULLS LAST, p.timestamp DESC
133
+ LIMIT ${Math.trunc(options.limit)}`;
134
+ }
135
+ function toNullableString(value) {
136
+ if (value === null || value === undefined) {
137
+ return null;
138
+ }
139
+ const normalized = String(value).trim();
140
+ return normalized.length > 0 ? normalized : null;
141
+ }
142
+ function toNumber(value) {
143
+ if (value === null || value === undefined) {
144
+ return null;
145
+ }
146
+ const numeric = Number(value);
147
+ return Number.isFinite(numeric) ? numeric : null;
148
+ }
149
+ function toBoolean(value) {
150
+ if (value === null || value === undefined) {
151
+ return null;
152
+ }
153
+ if (typeof value === "boolean") {
154
+ return value;
155
+ }
156
+ const normalized = String(value).trim().toLowerCase();
157
+ if (normalized === "true") {
158
+ return true;
159
+ }
160
+ if (normalized === "false") {
161
+ return false;
162
+ }
163
+ return null;
164
+ }
165
+ function normalizeCountryCode(value) {
166
+ return value === null ? null : value.trim().toUpperCase();
167
+ }
168
+ function isDachCountry(countryCode) {
169
+ return ["DE", "AT", "CH"].includes(countryCode ?? "");
170
+ }
171
+ function employeeCountFromBucket(bucket) {
172
+ switch ((bucket ?? "").trim()) {
173
+ case "200-499":
174
+ return 350;
175
+ case "501-1000":
176
+ return 750;
177
+ case "1001-5000":
178
+ return 2500;
179
+ case "5001-10.000":
180
+ return 7500;
181
+ case "10.000+":
182
+ return 10000;
183
+ default:
184
+ return 500;
185
+ }
186
+ }
187
+ export function buildDeelLeadPoolContactSql(options) {
188
+ const normalizedIds = options.contactIds
189
+ .map((contactId) => contactId.trim())
190
+ .filter((contactId) => contactId.length > 0);
191
+ if (normalizedIds.length === 0) {
192
+ throw new Error("At least one contactId is required to build the leadPool contact SQL.");
193
+ }
194
+ const contactList = normalizedIds.map((id) => `'${id.replaceAll("'", "\\'")}'`).join(", ");
195
+ const minScore = options.minEmailScore ?? 0;
196
+ return `SELECT
197
+ CAST(p.leadListId AS STRING) AS leadListId,
198
+ CAST(p.contactId AS STRING) AS contactId,
199
+ CAST(p.linkedin_contacts_companyId AS STRING) AS companyId,
200
+ CAST(p.firstName_cleaned AS STRING) AS firstName,
201
+ CAST(p.lastName_cleaned AS STRING) AS lastName,
202
+ CONCAT(
203
+ COALESCE(CAST(p.firstName_cleaned AS STRING), ''),
204
+ IF(p.firstName_cleaned IS NOT NULL AND p.lastName_cleaned IS NOT NULL, ' ', ''),
205
+ COALESCE(CAST(p.lastName_cleaned AS STRING), '')
206
+ ) AS fullName,
207
+ CAST(p.jobTitle AS STRING) AS jobTitle,
208
+ CAST(p.companyName AS STRING) AS companyName,
209
+ CAST(p.linkedin_companies_handle AS STRING) AS linkedin_companies_handle,
210
+ CONCAT('https://www.linkedin.com/company/', CAST(p.linkedin_contacts_companyId AS STRING)) AS companyUrl,
211
+ CAST(p.companySize AS STRING) AS companySize,
212
+ CAST(p.company_countryCode AS STRING) AS countryCode,
213
+ CAST(p.headquarters AS STRING) AS headquarters,
214
+ CAST(p.domain AS STRING) AS domain,
215
+ CAST(p.domain_linkedin AS STRING) AS domain_linkedin,
216
+ CAST(p.industry AS STRING) AS industry,
217
+ CAST(p.contact_location AS STRING) AS contact_location,
218
+ CAST(p.contact_companyLocation AS STRING) AS contact_companyLocation,
219
+ SAFE_CAST(p.tenureAtCompany AS FLOAT64) AS tenureAtCompany,
220
+ SAFE_CAST(p.tenureAtPosition AS FLOAT64) AS tenureAtPosition,
221
+ CAST(p.contact_summary AS STRING) AS contact_summary,
222
+ CAST(p.contact_titleDescription AS STRING) AS contact_titleDescription,
223
+ CAST(p.leadList_container_ts AS STRING) AS leadList_container_ts,
224
+ CAST(p.timestamp AS STRING) AS company_ts,
225
+ CAST(p.email AS STRING) AS email,
226
+ SAFE_CAST(p.emailScore AS INT64) AS emailScore,
227
+ SAFE_CAST(p.gender AS INT64) AS gender,
228
+ COALESCE(p.emails_accept_all, FALSE) AS emails_accept_all,
229
+ CAST(p.jobTitle AS STRING) AS jobTitleRaw,
230
+ CAST(p.domain AS STRING) AS domainRaw
231
+ FROM \`icpidentifier.SalesGPT.leadPool_new\` p
232
+ WHERE CAST(p.contactId AS STRING) IN (${contactList})
233
+ AND CAST(p.email AS STRING) IS NOT NULL
234
+ AND COALESCE(p.email_invalid, FALSE) = FALSE
235
+ AND COALESCE(p.email_bounced, FALSE) = FALSE
236
+ AND COALESCE(p.company_blacklisted, FALSE) = FALSE
237
+ AND COALESCE(p.company_inSequence, FALSE) = FALSE
238
+ AND COALESCE(p.contact_replied, FALSE) = FALSE
239
+ AND COALESCE(p.contact_bounced, FALSE) = FALSE
240
+ AND COALESCE(p.jobTitle_blacklisted, FALSE) = FALSE
241
+ AND COALESCE(p.domainBlacklisted, FALSE) = FALSE
242
+ AND CAST(p.domain AS STRING) IS NOT NULL
243
+ AND CAST(p.firstName_cleaned AS STRING) IS NOT NULL
244
+ AND CAST(p.lastName_cleaned AS STRING) IS NOT NULL
245
+ AND SAFE_CAST(p.emailScore AS INT64) >= ${Math.trunc(minScore)}
246
+ AND CAST(p.companySize AS STRING) IN ('200-499', '501-1000', '1001-5000', '5001-10.000', '10.000+')
247
+ QUALIFY ROW_NUMBER() OVER (
248
+ PARTITION BY LOWER(CAST(p.email AS STRING))
249
+ ORDER BY SAFE_CAST(p.emailScore AS INT64) DESC NULLS LAST, p.timestamp DESC, p.contact_ts DESC
250
+ ) = 1
251
+ ORDER BY RANDOM()`;
252
+ }
253
+ function buildGermanSalutation(row) {
254
+ const lastName = row.lastName?.trim() ?? "";
255
+ const firstName = row.firstName?.trim() ?? "";
256
+ if (row.gender === 1 && lastName.length > 0) {
257
+ return `Sehr geehrter Herr ${lastName},`;
258
+ }
259
+ if (row.gender === 2 && lastName.length > 0) {
260
+ return `Sehr geehrte Frau ${lastName},`;
261
+ }
262
+ if (firstName.length > 0 || lastName.length > 0) {
263
+ return `Guten Tag ${[firstName, lastName].filter((value) => value.length > 0).join(" ")},`;
264
+ }
265
+ return null;
266
+ }
267
+ function deriveLanguage(countryCode) {
268
+ return isDachCountry(countryCode) ? "de" : "en";
269
+ }
270
+ function deriveRegion(countryCode) {
271
+ if (isDachCountry(countryCode)) {
272
+ return "DACH";
273
+ }
274
+ return countryCode ?? "GLOBAL";
275
+ }
276
+ function computeLeadScore(row) {
277
+ const emailScore = row.emailScore ?? 70;
278
+ const segment = row.segment ?? "broader-hr";
279
+ const segmentBonus = segment === "leadership" ? 10 : segment === "payroll-people-services" ? 8 : 4;
280
+ const acceptAllPenalty = row.emails_accept_all ? 4 : 0;
281
+ const tenureBonus = (row.tenureAtCompany ?? 0) >= 1 ? 2 : 0;
282
+ return Math.max(60, Math.min(99, emailScore + segmentBonus + tenureBonus - acceptAllPenalty));
283
+ }
284
+ function gradeFromScore(score) {
285
+ if (score >= 90) {
286
+ return "A";
287
+ }
288
+ if (score >= 80) {
289
+ return "B";
290
+ }
291
+ if (score >= 70) {
292
+ return "C";
293
+ }
294
+ return "D";
295
+ }
296
+ export function normalizeDeelOutreachRows(rows) {
297
+ return rows.map((row) => {
298
+ const leadListQuery = toNullableString(row.leadListQuery);
299
+ const parsedQuery = leadListQuery === null ? null : parseHistoricalQuery(leadListQuery);
300
+ const countryCode = normalizeCountryCode(toNullableString(row.countryCode));
301
+ return {
302
+ leadListId: toNullableString(row.leadListId),
303
+ contactId: toNullableString(row.contactId),
304
+ companyId: toNullableString(row.companyId),
305
+ firstName: toNullableString(row.firstName),
306
+ lastName: toNullableString(row.lastName),
307
+ fullName: toNullableString(row.fullName),
308
+ jobTitle: toNullableString(row.jobTitle),
309
+ companyName: toNullableString(row.companyName),
310
+ linkedin_companies_handle: toNullableString(row.linkedin_companies_handle),
311
+ companyUrl: toNullableString(row.companyUrl),
312
+ companySize: toNullableString(row.companySize),
313
+ countryCode,
314
+ headquarters: toNullableString(row.headquarters),
315
+ domain: toNullableString(row.domain),
316
+ domain_linkedin: toNullableString(row.domain_linkedin),
317
+ industry: toNullableString(row.industry),
318
+ contact_location: toNullableString(row.contact_location),
319
+ contact_companyLocation: toNullableString(row.contact_companyLocation),
320
+ tenureAtCompany: toNumber(row.tenureAtCompany),
321
+ tenureAtPosition: toNumber(row.tenureAtPosition),
322
+ contact_summary: toNullableString(row.contact_summary),
323
+ contact_titleDescription: toNullableString(row.contact_titleDescription),
324
+ leadList_container_ts: toNullableString(row.leadList_container_ts),
325
+ company_ts: toNullableString(row.company_ts),
326
+ leadListQuery,
327
+ leadListQueryDecoded: parsedQuery?.decodedQuery ?? null,
328
+ leadListFunctionFilters: parsedQuery?.functionFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
329
+ leadListTitleFilters: parsedQuery?.titleFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
330
+ leadListRegionFilters: parsedQuery?.regionFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
331
+ leadListHeadquartersFilters: parsedQuery?.headquartersFilters
332
+ .filter((filter) => filter.selection === "INCLUDED")
333
+ .map((filter) => filter.text) ?? [],
334
+ leadListHeadcountFilters: parsedQuery?.headcountFilters.filter((filter) => filter.selection === "INCLUDED").map((filter) => filter.text) ?? [],
335
+ email: toNullableString(row.email),
336
+ emailScore: toNumber(row.emailScore),
337
+ gender: toNumber(row.gender),
338
+ emails_accept_all: toBoolean(row.emails_accept_all),
339
+ language: deriveLanguage(countryCode),
340
+ marketSegment: isDachCountry(countryCode) ? "dach" : "non-dach"
341
+ };
342
+ });
343
+ }
344
+ function rowToScoredLead(row) {
345
+ const contactName = row.fullName?.trim() ||
346
+ [row.firstName, row.lastName].filter((value) => value !== null && value.length > 0).join(" ").trim();
347
+ const title = row.jobTitle?.trim() ?? "";
348
+ const { segment, reason } = classifyDirectPathSegment(title);
349
+ const score = computeLeadScore({ ...row, segment, segmentReason: reason });
350
+ return {
351
+ companyName: row.companyName ?? row.domain ?? "Unknown Company",
352
+ domain: row.domain ?? row.domain_linkedin ?? "unknown.example",
353
+ industry: row.industry ?? "Unknown",
354
+ region: deriveRegion(row.countryCode),
355
+ employeeCount: employeeCountFromBucket(row.companySize),
356
+ contactName: contactName.length > 0 ? contactName : row.email ?? "Unknown Contact",
357
+ title: title.length > 0 ? title : "Human Resources",
358
+ email: row.email ?? "unknown@example.com",
359
+ source: "salesprompter-deel-leadlists",
360
+ signals: [
361
+ `language:${row.language}`,
362
+ `market:${row.marketSegment}`,
363
+ `segment:${segment}`,
364
+ row.countryCode ? `country:${row.countryCode}` : null
365
+ ].filter((value) => value !== null),
366
+ techStack: [],
367
+ crmFit: segment === "broader-hr" ? "medium" : "high",
368
+ outreachFit: score >= 85 ? "high" : "medium",
369
+ buyingStage: segment === "payroll-people-services" ? "solution-aware" : "problem-aware",
370
+ notes: [
371
+ row.leadListId ? `leadListId:${row.leadListId}` : null,
372
+ row.leadListRegionFilters.length > 0 ? `regions:${row.leadListRegionFilters.join(", ")}` : null
373
+ ].filter((value) => value !== null),
374
+ score,
375
+ grade: gradeFromScore(score),
376
+ rationale: [
377
+ `segment ${segment}: ${reason}`,
378
+ row.emailScore !== null ? `email score ${row.emailScore}` : "email score unavailable",
379
+ row.language === "de" ? "DACH lead routed to German outreach" : "non-DACH lead routed to English outreach"
380
+ ],
381
+ customVariables: {
382
+ language: row.language,
383
+ salesprompter_language: row.language,
384
+ salesprompter_market_scope: row.marketSegment,
385
+ salesprompter_country_code: row.countryCode,
386
+ salesprompter_segment: segment,
387
+ salesprompter_segment_reason: reason,
388
+ salesprompter_email_score: row.emailScore,
389
+ salesprompter_emails_accept_all: row.emails_accept_all,
390
+ salesprompter_leadlist_id: row.leadListId,
391
+ salesprompter_leadlist_regions: row.leadListRegionFilters.join(", "),
392
+ salesprompter_leadlist_titles: row.leadListTitleFilters.join(", "),
393
+ salesprompter_leadlist_functions: row.leadListFunctionFilters.join(", "),
394
+ salesprompter_linkedin_company_handle: row.linkedin_companies_handle,
395
+ gender: row.gender,
396
+ anrede_zeile: row.language === "de" ? buildGermanSalutation(row) : null
397
+ }
398
+ };
399
+ }
400
+ function average(values) {
401
+ if (values.length === 0) {
402
+ return null;
403
+ }
404
+ const total = values.reduce((sum, value) => sum + value, 0);
405
+ return Number((total / values.length).toFixed(2));
406
+ }
407
+ export function buildDeelOutreachPack(market, rows) {
408
+ const locales = {
409
+ de: [],
410
+ en: []
411
+ };
412
+ const countries = new Map();
413
+ const titles = new Map();
414
+ const segmentCounts = {
415
+ leadership: 0,
416
+ "payroll-people-services": 0,
417
+ "broader-hr": 0
418
+ };
419
+ const emailScores = {
420
+ de: [],
421
+ en: []
422
+ };
423
+ for (const row of rows) {
424
+ const { segment } = classifyDirectPathSegment(row.jobTitle ?? "");
425
+ segmentCounts[segment] += 1;
426
+ const country = row.countryCode ?? "unknown";
427
+ countries.set(country, (countries.get(country) ?? 0) + 1);
428
+ const title = row.jobTitle ?? "unknown";
429
+ titles.set(title, (titles.get(title) ?? 0) + 1);
430
+ if (row.emailScore !== null) {
431
+ emailScores[row.language].push(row.emailScore);
432
+ }
433
+ locales[row.language].push(rowToScoredLead({ ...row, segment }));
434
+ }
435
+ return {
436
+ vendor: "deel",
437
+ market,
438
+ total: rows.length,
439
+ locales,
440
+ summary: {
441
+ localeCounts: {
442
+ de: locales.de.length,
443
+ en: locales.en.length
444
+ },
445
+ segmentCounts,
446
+ countries: [...countries.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
447
+ averageEmailScoreByLocale: {
448
+ de: average(emailScores.de),
449
+ en: average(emailScores.en)
450
+ },
451
+ topTitles: [...titles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)
452
+ }
453
+ };
454
+ }