lacspace-leads 0.2.0 → 0.3.0
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 +86 -14
- package/dist/cli.js +426 -49
- package/dist/lib.cjs +355 -26
- package/dist/lib.d.cts +140 -6
- package/dist/lib.d.ts +140 -6
- package/dist/lib.js +344 -26
- package/package.json +8 -4
package/dist/lib.cjs
CHANGED
|
@@ -21,8 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var lib_exports = {};
|
|
22
22
|
__export(lib_exports, {
|
|
23
23
|
ALL_FIELDS: () => ALL_FIELDS,
|
|
24
|
+
CALLING_CODES: () => CALLING_CODES,
|
|
25
|
+
DEFAULT_FIELDS: () => DEFAULT_FIELDS,
|
|
24
26
|
ENRICHED_FIELDS: () => ENRICHED_FIELDS,
|
|
27
|
+
FIELD_PRESETS: () => FIELD_PRESETS,
|
|
25
28
|
LeadsError: () => LeadsError,
|
|
29
|
+
callingCode: () => callingCode,
|
|
30
|
+
cleanWebsite: () => cleanWebsite,
|
|
26
31
|
columnsOf: () => columnsOf,
|
|
27
32
|
composeQuery: () => composeQuery,
|
|
28
33
|
convertFile: () => convertFile,
|
|
@@ -30,19 +35,25 @@ __export(lib_exports, {
|
|
|
30
35
|
defaultFilename: () => defaultFilename,
|
|
31
36
|
detectFormat: () => detectFormat,
|
|
32
37
|
enrichContacts: () => enrichContacts,
|
|
38
|
+
expandQueries: () => expandQueries,
|
|
33
39
|
extractEmails: () => extractEmails,
|
|
34
40
|
extractSocials: () => extractSocials,
|
|
35
41
|
filterLeads: () => filterLeads,
|
|
36
42
|
mapsSearchUrl: () => mapsSearchUrl,
|
|
37
43
|
normalizeFields: () => normalizeFields,
|
|
44
|
+
normalizePhone: () => normalizePhone,
|
|
38
45
|
parseLatLng: () => parseLatLng,
|
|
39
46
|
parseRating: () => parseRating,
|
|
40
47
|
parseReviewCount: () => parseReviewCount,
|
|
41
48
|
readRows: () => readRows,
|
|
49
|
+
resolvePreset: () => resolvePreset,
|
|
42
50
|
scrapeLeads: () => scrapeLeads,
|
|
43
51
|
searchLeads: () => scrapeLeads,
|
|
52
|
+
searchLeadsBatch: () => searchLeadsBatch,
|
|
53
|
+
searchLeadsMulti: () => searchLeadsMulti,
|
|
44
54
|
serialize: () => serialize,
|
|
45
55
|
serializeRows: () => serializeRows,
|
|
56
|
+
sortLeads: () => sortLeads,
|
|
46
57
|
toRows: () => toRows
|
|
47
58
|
});
|
|
48
59
|
module.exports = __toCommonJS(lib_exports);
|
|
@@ -64,13 +75,42 @@ var ALL_FIELDS = [
|
|
|
64
75
|
"facebook",
|
|
65
76
|
"instagram",
|
|
66
77
|
"whatsapp",
|
|
78
|
+
"linkedin",
|
|
79
|
+
"twitter",
|
|
80
|
+
"youtube",
|
|
81
|
+
"tiktok",
|
|
82
|
+
"telegram",
|
|
67
83
|
"plusCode",
|
|
68
84
|
"latitude",
|
|
69
85
|
"longitude",
|
|
70
86
|
"hours",
|
|
71
87
|
"mapsUrl"
|
|
72
88
|
];
|
|
73
|
-
var ENRICHED_FIELDS = [
|
|
89
|
+
var ENRICHED_FIELDS = [
|
|
90
|
+
"email",
|
|
91
|
+
"facebook",
|
|
92
|
+
"instagram",
|
|
93
|
+
"whatsapp",
|
|
94
|
+
"linkedin",
|
|
95
|
+
"twitter",
|
|
96
|
+
"youtube",
|
|
97
|
+
"tiktok",
|
|
98
|
+
"telegram"
|
|
99
|
+
];
|
|
100
|
+
var FIELD_PRESETS = {
|
|
101
|
+
/** Just enough to place a business: name, phone, website. */
|
|
102
|
+
minimal: ["name", "phone", "website"],
|
|
103
|
+
/** Cold-outreach columns: who, how to reach them, where. */
|
|
104
|
+
outreach: ["name", "phone", "email", "website", "address"],
|
|
105
|
+
/** Contact-book columns incl. socials (implies enrichment). */
|
|
106
|
+
contact: ["name", "phone", "email", "website", "facebook", "instagram", "whatsapp"],
|
|
107
|
+
/** Map/geo columns for plotting. */
|
|
108
|
+
geo: ["name", "address", "latitude", "longitude", "plusCode", "mapsUrl"],
|
|
109
|
+
/** Everything Maps shows, no website enrichment. */
|
|
110
|
+
full: ALL_FIELDS.filter((f) => !ENRICHED_FIELDS.includes(f)),
|
|
111
|
+
/** Every field, including enriched ones. */
|
|
112
|
+
everything: [...ALL_FIELDS]
|
|
113
|
+
};
|
|
74
114
|
var DEFAULT_FIELDS = ALL_FIELDS.filter((f) => !ENRICHED_FIELDS.includes(f));
|
|
75
115
|
|
|
76
116
|
// src/query.ts
|
|
@@ -81,8 +121,38 @@ function composeQuery(opts) {
|
|
|
81
121
|
if (!type) throw new Error("A business `type` (or an explicit `query`) is required.");
|
|
82
122
|
return where ? `${type} in ${where}` : type;
|
|
83
123
|
}
|
|
84
|
-
function mapsSearchUrl(query) {
|
|
85
|
-
|
|
124
|
+
function mapsSearchUrl(query, opts = {}) {
|
|
125
|
+
const hl = (opts.hl ?? "en").split("-")[0] || "en";
|
|
126
|
+
const params = new URLSearchParams({ hl });
|
|
127
|
+
if (opts.gl) params.set("gl", opts.gl.toLowerCase());
|
|
128
|
+
return `https://www.google.com/maps/search/${encodeURIComponent(query)}?${params.toString()}`;
|
|
129
|
+
}
|
|
130
|
+
function expandQueries(opts) {
|
|
131
|
+
if (opts.query && opts.query.trim()) return [{ query: opts.query.trim() }];
|
|
132
|
+
const split = (s) => {
|
|
133
|
+
const parts = (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
134
|
+
return parts.length ? parts : [void 0];
|
|
135
|
+
};
|
|
136
|
+
const types = split(opts.type);
|
|
137
|
+
const cities = split(opts.city);
|
|
138
|
+
const areas = split(opts.area);
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const type of types) {
|
|
141
|
+
for (const city of cities) {
|
|
142
|
+
for (const area of areas) {
|
|
143
|
+
const entry = {};
|
|
144
|
+
if (type) entry.type = type;
|
|
145
|
+
if (city) entry.city = city;
|
|
146
|
+
if (area) entry.area = area;
|
|
147
|
+
out.push(entry);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function resolvePreset(name) {
|
|
154
|
+
if (!name) return void 0;
|
|
155
|
+
return FIELD_PRESETS[name.trim().toLowerCase()];
|
|
86
156
|
}
|
|
87
157
|
function normalizeFields(input) {
|
|
88
158
|
if (!input) return [...ALL_FIELDS];
|
|
@@ -123,12 +193,23 @@ function extractSocials(html) {
|
|
|
123
193
|
const hrefs = [...html.matchAll(/href\s*=\s*["']([^"']+)["']/gi)].map((m) => m[1] ?? "");
|
|
124
194
|
for (const href of hrefs) {
|
|
125
195
|
const u = href.trim();
|
|
126
|
-
if (!
|
|
196
|
+
if (!u) continue;
|
|
197
|
+
if (!out.facebook && /(^|\/\/|\.)facebook\.com\//i.test(u) && !/sharer|plugins|\/tr\?|dialog\//i.test(u)) {
|
|
127
198
|
out.facebook = absolutize(u);
|
|
128
|
-
} else if (!out.instagram && /(^|\/\/|\.)instagram\.com\//i.test(u)) {
|
|
199
|
+
} else if (!out.instagram && /(^|\/\/|\.)instagram\.com\//i.test(u) && !/\/(share|p)\//i.test(u)) {
|
|
129
200
|
out.instagram = absolutize(u);
|
|
130
201
|
} else if (!out.whatsapp && /(wa\.me\/|api\.whatsapp\.com\/|whatsapp:\/\/)/i.test(u)) {
|
|
131
202
|
out.whatsapp = u.startsWith("http") || u.startsWith("whatsapp") ? u : `https:${u}`;
|
|
203
|
+
} else if (!out.linkedin && /(^|\/\/|\.)linkedin\.com\/(company|in|school)\//i.test(u) && !/sharing|shareArticle/i.test(u)) {
|
|
204
|
+
out.linkedin = absolutize(u);
|
|
205
|
+
} else if (!out.twitter && /(^|\/\/|\.)(twitter|x)\.com\//i.test(u) && !/(intent|share|widgets|hashtag)\b/i.test(u)) {
|
|
206
|
+
out.twitter = absolutize(u);
|
|
207
|
+
} else if (!out.youtube && /(youtube\.com\/(channel|c|user|@)|youtu\.be\/)/i.test(u)) {
|
|
208
|
+
out.youtube = absolutize(u);
|
|
209
|
+
} else if (!out.tiktok && /(^|\/\/|\.)tiktok\.com\/@/i.test(u)) {
|
|
210
|
+
out.tiktok = absolutize(u);
|
|
211
|
+
} else if (!out.telegram && /(t\.me\/|telegram\.me\/)/i.test(u) && !/\/share\b/i.test(u)) {
|
|
212
|
+
out.telegram = absolutize(u);
|
|
132
213
|
}
|
|
133
214
|
}
|
|
134
215
|
return out;
|
|
@@ -179,11 +260,9 @@ async function enrichContacts(website, opts = {}) {
|
|
|
179
260
|
const email = extractEmails(page);
|
|
180
261
|
if (email) {
|
|
181
262
|
contacts.email = email;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!contacts
|
|
185
|
-
if (!contacts.instagram && s.instagram) contacts.instagram = s.instagram;
|
|
186
|
-
if (!contacts.whatsapp && s.whatsapp) contacts.whatsapp = s.whatsapp;
|
|
263
|
+
const s = extractSocials(page);
|
|
264
|
+
for (const k of Object.keys(s)) {
|
|
265
|
+
if (!contacts[k] && s[k]) contacts[k] = s[k];
|
|
187
266
|
}
|
|
188
267
|
break;
|
|
189
268
|
}
|
|
@@ -238,7 +317,148 @@ function dedupeLeads(leads, by = "website") {
|
|
|
238
317
|
return out;
|
|
239
318
|
}
|
|
240
319
|
|
|
320
|
+
// src/normalize.ts
|
|
321
|
+
var TRACKING_PARAMS = /^(utm_|fbclid$|gclid$|gbraid$|wbraid$|mc_|_hs|ref$|ref_src$|source$|igshid$|yclid$)/i;
|
|
322
|
+
function cleanWebsite(url) {
|
|
323
|
+
if (!url) return void 0;
|
|
324
|
+
let raw = url.trim();
|
|
325
|
+
if (!raw) return void 0;
|
|
326
|
+
const g = raw.match(/[?&](?:q|url)=([^&]+)/i);
|
|
327
|
+
if (g && /google\.[a-z.]+\/url/i.test(raw)) {
|
|
328
|
+
try {
|
|
329
|
+
raw = decodeURIComponent(g[1]);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
let u;
|
|
334
|
+
try {
|
|
335
|
+
u = new URL(raw.startsWith("http") ? raw : `https://${raw}`);
|
|
336
|
+
} catch {
|
|
337
|
+
return raw;
|
|
338
|
+
}
|
|
339
|
+
u.hash = "";
|
|
340
|
+
u.host = u.host.toLowerCase();
|
|
341
|
+
for (const key of [...u.searchParams.keys()]) {
|
|
342
|
+
if (TRACKING_PARAMS.test(key)) u.searchParams.delete(key);
|
|
343
|
+
}
|
|
344
|
+
let href = u.toString();
|
|
345
|
+
if (u.pathname === "/" && !u.search) href = href.replace(/\/$/, "");
|
|
346
|
+
return href;
|
|
347
|
+
}
|
|
348
|
+
var CALLING_CODES = {
|
|
349
|
+
NP: "977",
|
|
350
|
+
IN: "91",
|
|
351
|
+
US: "1",
|
|
352
|
+
CA: "1",
|
|
353
|
+
GB: "44",
|
|
354
|
+
AU: "61",
|
|
355
|
+
NZ: "64",
|
|
356
|
+
SG: "65",
|
|
357
|
+
MY: "60",
|
|
358
|
+
ID: "62",
|
|
359
|
+
TH: "66",
|
|
360
|
+
PH: "63",
|
|
361
|
+
VN: "84",
|
|
362
|
+
BD: "880",
|
|
363
|
+
PK: "92",
|
|
364
|
+
LK: "94",
|
|
365
|
+
AE: "971",
|
|
366
|
+
SA: "966",
|
|
367
|
+
QA: "974",
|
|
368
|
+
KW: "965",
|
|
369
|
+
BH: "973",
|
|
370
|
+
OM: "968",
|
|
371
|
+
CN: "86",
|
|
372
|
+
HK: "852",
|
|
373
|
+
JP: "81",
|
|
374
|
+
KR: "82",
|
|
375
|
+
DE: "49",
|
|
376
|
+
FR: "33",
|
|
377
|
+
ES: "34",
|
|
378
|
+
IT: "39",
|
|
379
|
+
NL: "31",
|
|
380
|
+
BE: "32",
|
|
381
|
+
CH: "41",
|
|
382
|
+
AT: "43",
|
|
383
|
+
SE: "46",
|
|
384
|
+
NO: "47",
|
|
385
|
+
DK: "45",
|
|
386
|
+
FI: "358",
|
|
387
|
+
IE: "353",
|
|
388
|
+
PT: "351",
|
|
389
|
+
PL: "48",
|
|
390
|
+
ZA: "27",
|
|
391
|
+
NG: "234",
|
|
392
|
+
KE: "254",
|
|
393
|
+
EG: "20",
|
|
394
|
+
BR: "55",
|
|
395
|
+
MX: "52",
|
|
396
|
+
AR: "54",
|
|
397
|
+
TR: "90"
|
|
398
|
+
};
|
|
399
|
+
function callingCode(country) {
|
|
400
|
+
if (!country) return void 0;
|
|
401
|
+
const c = country.trim().replace(/^\+/, "");
|
|
402
|
+
if (/^\d{1,4}$/.test(c)) return c;
|
|
403
|
+
return CALLING_CODES[c.toUpperCase()];
|
|
404
|
+
}
|
|
405
|
+
function normalizePhone(raw, country) {
|
|
406
|
+
if (!raw) return void 0;
|
|
407
|
+
const trimmed = raw.trim();
|
|
408
|
+
if (!trimmed) return void 0;
|
|
409
|
+
const isIntl = /^\+/.test(trimmed) || /^00\d/.test(trimmed);
|
|
410
|
+
let digits = trimmed.replace(/[^0-9]/g, "");
|
|
411
|
+
if (/^00\d/.test(trimmed)) digits = digits.replace(/^00/, "");
|
|
412
|
+
const cc = callingCode(country);
|
|
413
|
+
if (isIntl) {
|
|
414
|
+
return plausible(digits) ? `+${digits}` : trimmed;
|
|
415
|
+
}
|
|
416
|
+
if (!cc) return trimmed;
|
|
417
|
+
if (digits.startsWith(cc)) return plausible(digits) ? `+${digits}` : trimmed;
|
|
418
|
+
const local = digits.replace(/^0+/, "");
|
|
419
|
+
const e164 = `${cc}${local}`;
|
|
420
|
+
return plausible(e164) ? `+${e164}` : trimmed;
|
|
421
|
+
}
|
|
422
|
+
function plausible(digits) {
|
|
423
|
+
return digits.length >= 8 && digits.length <= 15;
|
|
424
|
+
}
|
|
425
|
+
var priceRank = (p) => p ? p.replace(/[^$€£₹¥₩]/g, "").length : 0;
|
|
426
|
+
function sortLeads(leads, by, dir) {
|
|
427
|
+
if (!by) return [...leads];
|
|
428
|
+
const desc = dir ? dir === "desc" : by !== "name";
|
|
429
|
+
const val = (l) => {
|
|
430
|
+
if (by === "name") return l.name?.toLowerCase();
|
|
431
|
+
if (by === "priceLevel") return l.priceLevel ? priceRank(l.priceLevel) : void 0;
|
|
432
|
+
return l[by];
|
|
433
|
+
};
|
|
434
|
+
return [...leads].map((lead, i) => ({ lead, i })).sort((a, b) => {
|
|
435
|
+
const av = val(a.lead);
|
|
436
|
+
const bv = val(b.lead);
|
|
437
|
+
const aMissing = av === void 0 || av === "";
|
|
438
|
+
const bMissing = bv === void 0 || bv === "";
|
|
439
|
+
if (aMissing && bMissing) return a.i - b.i;
|
|
440
|
+
if (aMissing) return 1;
|
|
441
|
+
if (bMissing) return -1;
|
|
442
|
+
let cmp = 0;
|
|
443
|
+
if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
|
|
444
|
+
else cmp = String(av).localeCompare(String(bv));
|
|
445
|
+
if (desc) cmp = -cmp;
|
|
446
|
+
return cmp !== 0 ? cmp : a.i - b.i;
|
|
447
|
+
}).map((x) => x.lead);
|
|
448
|
+
}
|
|
449
|
+
|
|
241
450
|
// src/scrape.ts
|
|
451
|
+
async function pool(items, n, fn, signal) {
|
|
452
|
+
let next = 0;
|
|
453
|
+
const workers = Array.from({ length: Math.max(1, Math.min(n, items.length)) }, async () => {
|
|
454
|
+
for (; ; ) {
|
|
455
|
+
const i = next++;
|
|
456
|
+
if (i >= items.length || signal?.aborted) return;
|
|
457
|
+
await fn(items[i], i);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
await Promise.all(workers);
|
|
461
|
+
}
|
|
242
462
|
var LeadsError = class extends Error {
|
|
243
463
|
code;
|
|
244
464
|
cause;
|
|
@@ -363,7 +583,13 @@ async function extractDetail(page, fields, fallbackName) {
|
|
|
363
583
|
}
|
|
364
584
|
if (fields.has("reviews")) {
|
|
365
585
|
const rv = await box.locator('span[aria-label*="review"]').first().getAttribute("aria-label").catch(() => null);
|
|
366
|
-
|
|
586
|
+
let count = parseReviewCount(rv);
|
|
587
|
+
if (count === void 0) {
|
|
588
|
+
const boxText = await box.innerText().catch(() => "");
|
|
589
|
+
const m = boxText.match(/\(([\d.,\s]+)\)/);
|
|
590
|
+
if (m) count = parseReviewCount(m[1]);
|
|
591
|
+
}
|
|
592
|
+
lead.reviews = count;
|
|
367
593
|
}
|
|
368
594
|
}
|
|
369
595
|
}
|
|
@@ -415,12 +641,15 @@ async function scrapeLeads(opts) {
|
|
|
415
641
|
onProgress?.(`searching Google Maps for "${query}"\u2026`);
|
|
416
642
|
const browser = await launchBrowser(headless);
|
|
417
643
|
try {
|
|
644
|
+
const locale = opts.locale ?? "en-US";
|
|
418
645
|
const context = await browser.newContext({
|
|
419
646
|
viewport: { width: 1280, height: 900 },
|
|
420
|
-
locale
|
|
647
|
+
locale
|
|
421
648
|
});
|
|
422
649
|
const page = await context.newPage();
|
|
423
|
-
|
|
650
|
+
const urlOpts = { hl: locale };
|
|
651
|
+
if (opts.region) urlOpts.gl = opts.region;
|
|
652
|
+
await page.goto(mapsSearchUrl(query, urlOpts), { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
424
653
|
await dismissConsent(page);
|
|
425
654
|
await loadResults(page, limit, delayMs, onProgress, signal);
|
|
426
655
|
if (signal?.aborted) throw new LeadsError("Search aborted.", "ABORTED");
|
|
@@ -470,22 +699,42 @@ async function scrapeLeads(opts) {
|
|
|
470
699
|
}
|
|
471
700
|
}
|
|
472
701
|
onProgress?.(`collected ${leads.length} listing${leads.length === 1 ? "" : "s"}.`);
|
|
702
|
+
if (opts.cleanUrls ?? true) {
|
|
703
|
+
for (const lead of leads) {
|
|
704
|
+
if (lead.website) lead.website = cleanWebsite(lead.website);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
473
707
|
leads = dedupeLeads(leads, opts.dedupe ?? "website");
|
|
474
708
|
if (wantEnrich) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
709
|
+
const socialFields = ENRICHED_FIELDS.filter((f) => f !== "email");
|
|
710
|
+
let done = 0;
|
|
711
|
+
await pool(
|
|
712
|
+
leads,
|
|
713
|
+
Math.max(1, Math.trunc(opts.concurrency ?? 3)),
|
|
714
|
+
async (lead) => {
|
|
715
|
+
if (overBudget()) return;
|
|
716
|
+
const site = lead.website;
|
|
717
|
+
if (site) {
|
|
718
|
+
onProgress?.(`enriching ${++done}/${leads.length}: ${lead.name ?? site}\u2026`);
|
|
719
|
+
const c = await enrichContacts(site);
|
|
720
|
+
if (fields.has("email") && c.email) lead.email = c.email;
|
|
721
|
+
for (const f of socialFields) {
|
|
722
|
+
const v = c[f];
|
|
723
|
+
if (fields.has(f) && v) lead[f] = v;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!fields.has("website")) delete lead.website;
|
|
727
|
+
},
|
|
728
|
+
signal
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (opts.country && fields.has("phone")) {
|
|
732
|
+
for (const lead of leads) {
|
|
733
|
+
if (lead.phone) lead.phone = normalizePhone(lead.phone, opts.country);
|
|
486
734
|
}
|
|
487
735
|
}
|
|
488
736
|
if (opts.filters) leads = filterLeads(leads, opts.filters);
|
|
737
|
+
if (opts.sort) leads = sortLeads(leads, opts.sort, opts.sortDir);
|
|
489
738
|
onProgress?.(`collected ${leads.length} lead${leads.length === 1 ? "" : "s"}.`);
|
|
490
739
|
return leads;
|
|
491
740
|
} finally {
|
|
@@ -494,6 +743,69 @@ async function scrapeLeads(opts) {
|
|
|
494
743
|
}
|
|
495
744
|
}
|
|
496
745
|
|
|
746
|
+
// src/batch.ts
|
|
747
|
+
async function searchLeadsBatch(queries, opts = {}) {
|
|
748
|
+
const {
|
|
749
|
+
total,
|
|
750
|
+
onProgress,
|
|
751
|
+
signal,
|
|
752
|
+
type: _t,
|
|
753
|
+
city: _c,
|
|
754
|
+
area: _a,
|
|
755
|
+
query: _q,
|
|
756
|
+
...shared
|
|
757
|
+
} = opts;
|
|
758
|
+
void _t;
|
|
759
|
+
void _c;
|
|
760
|
+
void _a;
|
|
761
|
+
void _q;
|
|
762
|
+
let merged = [];
|
|
763
|
+
const seenDedupe = shared.dedupe ?? "website";
|
|
764
|
+
for (let i = 0; i < queries.length; i++) {
|
|
765
|
+
if (signal?.aborted) break;
|
|
766
|
+
const q = queries[i];
|
|
767
|
+
const label = (() => {
|
|
768
|
+
try {
|
|
769
|
+
return composeQuery(q);
|
|
770
|
+
} catch {
|
|
771
|
+
return q.query ?? q.type ?? "search";
|
|
772
|
+
}
|
|
773
|
+
})();
|
|
774
|
+
onProgress?.(`[${i + 1}/${queries.length}] ${label}`);
|
|
775
|
+
const subOpts = {
|
|
776
|
+
...shared,
|
|
777
|
+
type: q.type ?? "",
|
|
778
|
+
// Defer sorting to the merged pass; keep per-search dedupe/filter local.
|
|
779
|
+
...q.query ? { query: q.query } : {},
|
|
780
|
+
...q.city ? { city: q.city } : {},
|
|
781
|
+
...q.area ? { area: q.area } : {}
|
|
782
|
+
};
|
|
783
|
+
delete subOpts.sort;
|
|
784
|
+
if (signal) subOpts.signal = signal;
|
|
785
|
+
if (onProgress) subOpts.onProgress = (m) => onProgress(` ${m}`);
|
|
786
|
+
try {
|
|
787
|
+
const found = await scrapeLeads(subOpts);
|
|
788
|
+
merged = merged.concat(found);
|
|
789
|
+
} catch (err) {
|
|
790
|
+
onProgress?.(` ! skipped "${label}": ${err.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
merged = dedupeLeads(merged, seenDedupe);
|
|
794
|
+
if (opts.sort) merged = sortLeads(merged, opts.sort, opts.sortDir);
|
|
795
|
+
if (total !== void 0 && total > 0) merged = merged.slice(0, total);
|
|
796
|
+
onProgress?.(`merged ${merged.length} unique lead${merged.length === 1 ? "" : "s"}.`);
|
|
797
|
+
return merged;
|
|
798
|
+
}
|
|
799
|
+
async function searchLeadsMulti(opts) {
|
|
800
|
+
const queries = expandQueries({
|
|
801
|
+
...opts.type ? { type: opts.type } : {},
|
|
802
|
+
...opts.city ? { city: opts.city } : {},
|
|
803
|
+
...opts.area ? { area: opts.area } : {},
|
|
804
|
+
...opts.query ? { query: opts.query } : {}
|
|
805
|
+
});
|
|
806
|
+
return searchLeadsBatch(queries, opts);
|
|
807
|
+
}
|
|
808
|
+
|
|
497
809
|
// src/export.ts
|
|
498
810
|
var import_csv = require("@lacspace/csv");
|
|
499
811
|
var import_xlsx = require("@lacspace/xlsx");
|
|
@@ -510,6 +822,11 @@ var HEADERS = {
|
|
|
510
822
|
facebook: "Facebook",
|
|
511
823
|
instagram: "Instagram",
|
|
512
824
|
whatsapp: "WhatsApp",
|
|
825
|
+
linkedin: "LinkedIn",
|
|
826
|
+
twitter: "Twitter/X",
|
|
827
|
+
youtube: "YouTube",
|
|
828
|
+
tiktok: "TikTok",
|
|
829
|
+
telegram: "Telegram",
|
|
513
830
|
plusCode: "Plus Code",
|
|
514
831
|
latitude: "Latitude",
|
|
515
832
|
longitude: "Longitude",
|
|
@@ -527,13 +844,14 @@ function toRows(leads, fields = ALL_FIELDS) {
|
|
|
527
844
|
});
|
|
528
845
|
}
|
|
529
846
|
function serialize(leads, format, fields = ALL_FIELDS, opts = {}) {
|
|
530
|
-
if (format === "json") {
|
|
847
|
+
if (format === "json" || format === "ndjson") {
|
|
531
848
|
const picked = leads.map((lead) => {
|
|
532
849
|
const o = {};
|
|
533
850
|
for (const f of fields) if (lead[f] !== void 0) o[f] = lead[f];
|
|
534
851
|
return o;
|
|
535
852
|
});
|
|
536
|
-
|
|
853
|
+
const data = format === "ndjson" ? picked.map((o) => JSON.stringify(o)).join("\n") + (picked.length ? "\n" : "") : JSON.stringify(picked, null, 2);
|
|
854
|
+
return { data, binary: false };
|
|
537
855
|
}
|
|
538
856
|
const rows = toRows(leads, fields);
|
|
539
857
|
if (format === "csv") {
|
|
@@ -627,8 +945,13 @@ async function convertFile(input, opts = {}) {
|
|
|
627
945
|
// Annotate the CommonJS export names for ESM import in node:
|
|
628
946
|
0 && (module.exports = {
|
|
629
947
|
ALL_FIELDS,
|
|
948
|
+
CALLING_CODES,
|
|
949
|
+
DEFAULT_FIELDS,
|
|
630
950
|
ENRICHED_FIELDS,
|
|
951
|
+
FIELD_PRESETS,
|
|
631
952
|
LeadsError,
|
|
953
|
+
callingCode,
|
|
954
|
+
cleanWebsite,
|
|
632
955
|
columnsOf,
|
|
633
956
|
composeQuery,
|
|
634
957
|
convertFile,
|
|
@@ -636,18 +959,24 @@ async function convertFile(input, opts = {}) {
|
|
|
636
959
|
defaultFilename,
|
|
637
960
|
detectFormat,
|
|
638
961
|
enrichContacts,
|
|
962
|
+
expandQueries,
|
|
639
963
|
extractEmails,
|
|
640
964
|
extractSocials,
|
|
641
965
|
filterLeads,
|
|
642
966
|
mapsSearchUrl,
|
|
643
967
|
normalizeFields,
|
|
968
|
+
normalizePhone,
|
|
644
969
|
parseLatLng,
|
|
645
970
|
parseRating,
|
|
646
971
|
parseReviewCount,
|
|
647
972
|
readRows,
|
|
973
|
+
resolvePreset,
|
|
648
974
|
scrapeLeads,
|
|
649
975
|
searchLeads,
|
|
976
|
+
searchLeadsBatch,
|
|
977
|
+
searchLeadsMulti,
|
|
650
978
|
serialize,
|
|
651
979
|
serializeRows,
|
|
980
|
+
sortLeads,
|
|
652
981
|
toRows
|
|
653
982
|
});
|