lacspace-leads 0.2.1 → 1.2.1
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 +115 -16
- package/dist/cli.js +680 -126
- package/dist/lib.cjs +577 -99
- package/dist/lib.d.cts +221 -8
- package/dist/lib.d.ts +221 -8
- package/dist/lib.js +554 -94
- package/package.json +7 -2
package/dist/lib.cjs
CHANGED
|
@@ -21,29 +21,47 @@ 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,
|
|
33
|
+
computeStats: () => computeStats,
|
|
28
34
|
convertFile: () => convertFile,
|
|
29
35
|
dedupeLeads: () => dedupeLeads,
|
|
30
36
|
defaultFilename: () => defaultFilename,
|
|
31
37
|
detectFormat: () => detectFormat,
|
|
38
|
+
emailDomain: () => emailDomain,
|
|
39
|
+
emailFormatValid: () => emailFormatValid,
|
|
32
40
|
enrichContacts: () => enrichContacts,
|
|
41
|
+
expandQueries: () => expandQueries,
|
|
33
42
|
extractEmails: () => extractEmails,
|
|
34
43
|
extractSocials: () => extractSocials,
|
|
35
44
|
filterLeads: () => filterLeads,
|
|
36
45
|
mapsSearchUrl: () => mapsSearchUrl,
|
|
37
46
|
normalizeFields: () => normalizeFields,
|
|
47
|
+
normalizePhone: () => normalizePhone,
|
|
38
48
|
parseLatLng: () => parseLatLng,
|
|
39
49
|
parseRating: () => parseRating,
|
|
40
50
|
parseReviewCount: () => parseReviewCount,
|
|
41
51
|
readRows: () => readRows,
|
|
52
|
+
resolvePreset: () => resolvePreset,
|
|
53
|
+
rowsToLeads: () => rowsToLeads,
|
|
42
54
|
scrapeLeads: () => scrapeLeads,
|
|
43
55
|
searchLeads: () => scrapeLeads,
|
|
56
|
+
searchLeadsBatch: () => searchLeadsBatch,
|
|
57
|
+
searchLeadsDetailed: () => searchLeadsDetailed,
|
|
58
|
+
searchLeadsMulti: () => searchLeadsMulti,
|
|
44
59
|
serialize: () => serialize,
|
|
45
60
|
serializeRows: () => serializeRows,
|
|
46
|
-
|
|
61
|
+
sortLeads: () => sortLeads,
|
|
62
|
+
toRows: () => toRows,
|
|
63
|
+
verifyEmail: () => verifyEmail,
|
|
64
|
+
verifyEmails: () => verifyEmails
|
|
47
65
|
});
|
|
48
66
|
module.exports = __toCommonJS(lib_exports);
|
|
49
67
|
|
|
@@ -64,13 +82,44 @@ var ALL_FIELDS = [
|
|
|
64
82
|
"facebook",
|
|
65
83
|
"instagram",
|
|
66
84
|
"whatsapp",
|
|
85
|
+
"linkedin",
|
|
86
|
+
"twitter",
|
|
87
|
+
"youtube",
|
|
88
|
+
"tiktok",
|
|
89
|
+
"telegram",
|
|
90
|
+
"emailStatus",
|
|
67
91
|
"plusCode",
|
|
68
92
|
"latitude",
|
|
69
93
|
"longitude",
|
|
70
94
|
"hours",
|
|
71
95
|
"mapsUrl"
|
|
72
96
|
];
|
|
73
|
-
var ENRICHED_FIELDS = [
|
|
97
|
+
var ENRICHED_FIELDS = [
|
|
98
|
+
"email",
|
|
99
|
+
"facebook",
|
|
100
|
+
"instagram",
|
|
101
|
+
"whatsapp",
|
|
102
|
+
"linkedin",
|
|
103
|
+
"twitter",
|
|
104
|
+
"youtube",
|
|
105
|
+
"tiktok",
|
|
106
|
+
"telegram",
|
|
107
|
+
"emailStatus"
|
|
108
|
+
];
|
|
109
|
+
var FIELD_PRESETS = {
|
|
110
|
+
/** Just enough to place a business: name, phone, website. */
|
|
111
|
+
minimal: ["name", "phone", "website"],
|
|
112
|
+
/** Cold-outreach columns: who, how to reach them, where. */
|
|
113
|
+
outreach: ["name", "phone", "email", "website", "address"],
|
|
114
|
+
/** Contact-book columns incl. socials (implies enrichment). */
|
|
115
|
+
contact: ["name", "phone", "email", "website", "facebook", "instagram", "whatsapp"],
|
|
116
|
+
/** Map/geo columns for plotting. */
|
|
117
|
+
geo: ["name", "address", "latitude", "longitude", "plusCode", "mapsUrl"],
|
|
118
|
+
/** Everything Maps shows, no website enrichment. */
|
|
119
|
+
full: ALL_FIELDS.filter((f) => !ENRICHED_FIELDS.includes(f)),
|
|
120
|
+
/** Every field, including enriched ones. */
|
|
121
|
+
everything: [...ALL_FIELDS]
|
|
122
|
+
};
|
|
74
123
|
var DEFAULT_FIELDS = ALL_FIELDS.filter((f) => !ENRICHED_FIELDS.includes(f));
|
|
75
124
|
|
|
76
125
|
// src/query.ts
|
|
@@ -81,8 +130,38 @@ function composeQuery(opts) {
|
|
|
81
130
|
if (!type) throw new Error("A business `type` (or an explicit `query`) is required.");
|
|
82
131
|
return where ? `${type} in ${where}` : type;
|
|
83
132
|
}
|
|
84
|
-
function mapsSearchUrl(query) {
|
|
85
|
-
|
|
133
|
+
function mapsSearchUrl(query, opts = {}) {
|
|
134
|
+
const hl = (opts.hl ?? "en").split("-")[0] || "en";
|
|
135
|
+
const params = new URLSearchParams({ hl });
|
|
136
|
+
if (opts.gl) params.set("gl", opts.gl.toLowerCase());
|
|
137
|
+
return `https://www.google.com/maps/search/${encodeURIComponent(query)}?${params.toString()}`;
|
|
138
|
+
}
|
|
139
|
+
function expandQueries(opts) {
|
|
140
|
+
if (opts.query && opts.query.trim()) return [{ query: opts.query.trim() }];
|
|
141
|
+
const split = (s) => {
|
|
142
|
+
const parts = (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
143
|
+
return parts.length ? parts : [void 0];
|
|
144
|
+
};
|
|
145
|
+
const types = split(opts.type);
|
|
146
|
+
const cities = split(opts.city);
|
|
147
|
+
const areas = split(opts.area);
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const type of types) {
|
|
150
|
+
for (const city of cities) {
|
|
151
|
+
for (const area of areas) {
|
|
152
|
+
const entry = {};
|
|
153
|
+
if (type) entry.type = type;
|
|
154
|
+
if (city) entry.city = city;
|
|
155
|
+
if (area) entry.area = area;
|
|
156
|
+
out.push(entry);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function resolvePreset(name) {
|
|
163
|
+
if (!name) return void 0;
|
|
164
|
+
return FIELD_PRESETS[name.trim().toLowerCase()];
|
|
86
165
|
}
|
|
87
166
|
function normalizeFields(input) {
|
|
88
167
|
if (!input) return [...ALL_FIELDS];
|
|
@@ -123,12 +202,23 @@ function extractSocials(html) {
|
|
|
123
202
|
const hrefs = [...html.matchAll(/href\s*=\s*["']([^"']+)["']/gi)].map((m) => m[1] ?? "");
|
|
124
203
|
for (const href of hrefs) {
|
|
125
204
|
const u = href.trim();
|
|
126
|
-
if (!
|
|
205
|
+
if (!u) continue;
|
|
206
|
+
if (!out.facebook && /(^|\/\/|\.)facebook\.com\//i.test(u) && !/sharer|plugins|\/tr\?|dialog\//i.test(u)) {
|
|
127
207
|
out.facebook = absolutize(u);
|
|
128
|
-
} else if (!out.instagram && /(^|\/\/|\.)instagram\.com\//i.test(u)) {
|
|
208
|
+
} else if (!out.instagram && /(^|\/\/|\.)instagram\.com\//i.test(u) && !/\/(share|p)\//i.test(u)) {
|
|
129
209
|
out.instagram = absolutize(u);
|
|
130
210
|
} else if (!out.whatsapp && /(wa\.me\/|api\.whatsapp\.com\/|whatsapp:\/\/)/i.test(u)) {
|
|
131
211
|
out.whatsapp = u.startsWith("http") || u.startsWith("whatsapp") ? u : `https:${u}`;
|
|
212
|
+
} else if (!out.linkedin && /(^|\/\/|\.)linkedin\.com\/(company|in|school)\//i.test(u) && !/sharing|shareArticle/i.test(u)) {
|
|
213
|
+
out.linkedin = absolutize(u);
|
|
214
|
+
} else if (!out.twitter && /(^|\/\/|\.)(twitter|x)\.com\//i.test(u) && !/(intent|share|widgets|hashtag)\b/i.test(u)) {
|
|
215
|
+
out.twitter = absolutize(u);
|
|
216
|
+
} else if (!out.youtube && /(youtube\.com\/(channel|c|user|@)|youtu\.be\/)/i.test(u)) {
|
|
217
|
+
out.youtube = absolutize(u);
|
|
218
|
+
} else if (!out.tiktok && /(^|\/\/|\.)tiktok\.com\/@/i.test(u)) {
|
|
219
|
+
out.tiktok = absolutize(u);
|
|
220
|
+
} else if (!out.telegram && /(t\.me\/|telegram\.me\/)/i.test(u) && !/\/share\b/i.test(u)) {
|
|
221
|
+
out.telegram = absolutize(u);
|
|
132
222
|
}
|
|
133
223
|
}
|
|
134
224
|
return out;
|
|
@@ -179,11 +269,9 @@ async function enrichContacts(website, opts = {}) {
|
|
|
179
269
|
const email = extractEmails(page);
|
|
180
270
|
if (email) {
|
|
181
271
|
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;
|
|
272
|
+
const s = extractSocials(page);
|
|
273
|
+
for (const k of Object.keys(s)) {
|
|
274
|
+
if (!contacts[k] && s[k]) contacts[k] = s[k];
|
|
187
275
|
}
|
|
188
276
|
break;
|
|
189
277
|
}
|
|
@@ -204,6 +292,7 @@ function filterLeads(leads, filters = {}) {
|
|
|
204
292
|
if (filters.hasPhone && !l.phone) return false;
|
|
205
293
|
if (filters.hasWebsite && !l.website) return false;
|
|
206
294
|
if (filters.hasEmail && !l.email) return false;
|
|
295
|
+
if (filters.hasValidEmail && l.emailStatus !== "valid") return false;
|
|
207
296
|
return true;
|
|
208
297
|
});
|
|
209
298
|
}
|
|
@@ -211,7 +300,8 @@ function dedupeKey(lead, by) {
|
|
|
211
300
|
if (by === "none") return void 0;
|
|
212
301
|
if (by === "website") return normalizeHost(lead.website);
|
|
213
302
|
if (by === "phone") return lead.phone?.replace(/[^0-9]/g, "") || void 0;
|
|
214
|
-
return lead.name?.trim().toLowerCase() || void 0;
|
|
303
|
+
if (by === "name") return lead.name?.trim().toLowerCase() || void 0;
|
|
304
|
+
return normalizeHost(lead.website) ?? (lead.phone?.replace(/[^0-9]/g, "") || void 0) ?? (lead.name?.trim().toLowerCase() || void 0);
|
|
215
305
|
}
|
|
216
306
|
function normalizeHost(url) {
|
|
217
307
|
if (!url) return void 0;
|
|
@@ -238,7 +328,300 @@ function dedupeLeads(leads, by = "website") {
|
|
|
238
328
|
return out;
|
|
239
329
|
}
|
|
240
330
|
|
|
331
|
+
// src/normalize.ts
|
|
332
|
+
var TRACKING_PARAMS = /^(utm_|fbclid$|gclid$|gbraid$|wbraid$|mc_|_hs|ref$|ref_src$|source$|igshid$|yclid$)/i;
|
|
333
|
+
function cleanWebsite(url) {
|
|
334
|
+
if (!url) return void 0;
|
|
335
|
+
let raw = url.trim();
|
|
336
|
+
if (!raw) return void 0;
|
|
337
|
+
const g = raw.match(/[?&](?:q|url)=([^&]+)/i);
|
|
338
|
+
if (g && /google\.[a-z.]+\/url/i.test(raw)) {
|
|
339
|
+
try {
|
|
340
|
+
raw = decodeURIComponent(g[1]);
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
let u;
|
|
345
|
+
try {
|
|
346
|
+
u = new URL(raw.startsWith("http") ? raw : `https://${raw}`);
|
|
347
|
+
} catch {
|
|
348
|
+
return raw;
|
|
349
|
+
}
|
|
350
|
+
u.hash = "";
|
|
351
|
+
u.host = u.host.toLowerCase();
|
|
352
|
+
for (const key of [...u.searchParams.keys()]) {
|
|
353
|
+
if (TRACKING_PARAMS.test(key)) u.searchParams.delete(key);
|
|
354
|
+
}
|
|
355
|
+
let href = u.toString();
|
|
356
|
+
if (u.pathname === "/" && !u.search) href = href.replace(/\/$/, "");
|
|
357
|
+
return href;
|
|
358
|
+
}
|
|
359
|
+
var CALLING_CODES = {
|
|
360
|
+
NP: "977",
|
|
361
|
+
IN: "91",
|
|
362
|
+
US: "1",
|
|
363
|
+
CA: "1",
|
|
364
|
+
GB: "44",
|
|
365
|
+
AU: "61",
|
|
366
|
+
NZ: "64",
|
|
367
|
+
SG: "65",
|
|
368
|
+
MY: "60",
|
|
369
|
+
ID: "62",
|
|
370
|
+
TH: "66",
|
|
371
|
+
PH: "63",
|
|
372
|
+
VN: "84",
|
|
373
|
+
BD: "880",
|
|
374
|
+
PK: "92",
|
|
375
|
+
LK: "94",
|
|
376
|
+
AE: "971",
|
|
377
|
+
SA: "966",
|
|
378
|
+
QA: "974",
|
|
379
|
+
KW: "965",
|
|
380
|
+
BH: "973",
|
|
381
|
+
OM: "968",
|
|
382
|
+
CN: "86",
|
|
383
|
+
HK: "852",
|
|
384
|
+
JP: "81",
|
|
385
|
+
KR: "82",
|
|
386
|
+
DE: "49",
|
|
387
|
+
FR: "33",
|
|
388
|
+
ES: "34",
|
|
389
|
+
IT: "39",
|
|
390
|
+
NL: "31",
|
|
391
|
+
BE: "32",
|
|
392
|
+
CH: "41",
|
|
393
|
+
AT: "43",
|
|
394
|
+
SE: "46",
|
|
395
|
+
NO: "47",
|
|
396
|
+
DK: "45",
|
|
397
|
+
FI: "358",
|
|
398
|
+
IE: "353",
|
|
399
|
+
PT: "351",
|
|
400
|
+
PL: "48",
|
|
401
|
+
ZA: "27",
|
|
402
|
+
NG: "234",
|
|
403
|
+
KE: "254",
|
|
404
|
+
EG: "20",
|
|
405
|
+
BR: "55",
|
|
406
|
+
MX: "52",
|
|
407
|
+
AR: "54",
|
|
408
|
+
TR: "90"
|
|
409
|
+
};
|
|
410
|
+
function callingCode(country) {
|
|
411
|
+
if (!country) return void 0;
|
|
412
|
+
const c = country.trim().replace(/^\+/, "");
|
|
413
|
+
if (/^\d{1,4}$/.test(c)) return c;
|
|
414
|
+
return CALLING_CODES[c.toUpperCase()];
|
|
415
|
+
}
|
|
416
|
+
function normalizePhone(raw, country) {
|
|
417
|
+
if (!raw) return void 0;
|
|
418
|
+
const trimmed = raw.trim();
|
|
419
|
+
if (!trimmed) return void 0;
|
|
420
|
+
const isIntl = /^\+/.test(trimmed) || /^00\d/.test(trimmed);
|
|
421
|
+
let digits = trimmed.replace(/[^0-9]/g, "");
|
|
422
|
+
if (/^00\d/.test(trimmed)) digits = digits.replace(/^00/, "");
|
|
423
|
+
const cc = callingCode(country);
|
|
424
|
+
if (isIntl) {
|
|
425
|
+
return plausible(digits) ? `+${digits}` : trimmed;
|
|
426
|
+
}
|
|
427
|
+
if (!cc) return trimmed;
|
|
428
|
+
if (digits.startsWith(cc)) return plausible(digits) ? `+${digits}` : trimmed;
|
|
429
|
+
const local = digits.replace(/^0+/, "");
|
|
430
|
+
const e164 = `${cc}${local}`;
|
|
431
|
+
return plausible(e164) ? `+${e164}` : trimmed;
|
|
432
|
+
}
|
|
433
|
+
function plausible(digits) {
|
|
434
|
+
return digits.length >= 8 && digits.length <= 15;
|
|
435
|
+
}
|
|
436
|
+
var priceRank = (p) => p ? p.replace(/[^$€£₹¥₩]/g, "").length : 0;
|
|
437
|
+
function sortLeads(leads, by, dir) {
|
|
438
|
+
if (!by) return [...leads];
|
|
439
|
+
const desc = dir ? dir === "desc" : by !== "name";
|
|
440
|
+
const val = (l) => {
|
|
441
|
+
if (by === "name") return l.name?.toLowerCase();
|
|
442
|
+
if (by === "priceLevel") return l.priceLevel ? priceRank(l.priceLevel) : void 0;
|
|
443
|
+
return l[by];
|
|
444
|
+
};
|
|
445
|
+
return [...leads].map((lead, i) => ({ lead, i })).sort((a, b) => {
|
|
446
|
+
const av = val(a.lead);
|
|
447
|
+
const bv = val(b.lead);
|
|
448
|
+
const aMissing = av === void 0 || av === "";
|
|
449
|
+
const bMissing = bv === void 0 || bv === "";
|
|
450
|
+
if (aMissing && bMissing) return a.i - b.i;
|
|
451
|
+
if (aMissing) return 1;
|
|
452
|
+
if (bMissing) return -1;
|
|
453
|
+
let cmp = 0;
|
|
454
|
+
if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
|
|
455
|
+
else cmp = String(av).localeCompare(String(bv));
|
|
456
|
+
if (desc) cmp = -cmp;
|
|
457
|
+
return cmp !== 0 ? cmp : a.i - b.i;
|
|
458
|
+
}).map((x) => x.lead);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/verify.ts
|
|
462
|
+
var import_promises = require("dns/promises");
|
|
463
|
+
var EMAIL_SHAPE = /^[^\s@]+@([^\s@.]+\.)+[^\s@.]{2,}$/;
|
|
464
|
+
function emailFormatValid(email) {
|
|
465
|
+
return !!email && EMAIL_SHAPE.test(email.trim());
|
|
466
|
+
}
|
|
467
|
+
function emailDomain(email) {
|
|
468
|
+
if (!email) return void 0;
|
|
469
|
+
const at = email.lastIndexOf("@");
|
|
470
|
+
return at >= 0 ? email.slice(at + 1).trim().toLowerCase() || void 0 : void 0;
|
|
471
|
+
}
|
|
472
|
+
async function domainHasMail(domain, cache, timeoutMs) {
|
|
473
|
+
const cached = cache.get(domain);
|
|
474
|
+
if (cached !== void 0) return cached;
|
|
475
|
+
const timer = new Promise((res) => setTimeout(() => res(false), timeoutMs));
|
|
476
|
+
const lookup = (0, import_promises.resolveMx)(domain).then((records) => Array.isArray(records) && records.length > 0).catch(() => false);
|
|
477
|
+
const ok = await Promise.race([lookup, timer]);
|
|
478
|
+
cache.set(domain, ok);
|
|
479
|
+
return ok;
|
|
480
|
+
}
|
|
481
|
+
async function verifyEmail(email, opts = {}) {
|
|
482
|
+
if (!email) return "unknown";
|
|
483
|
+
if (!emailFormatValid(email)) return "invalid-format";
|
|
484
|
+
const domain = emailDomain(email);
|
|
485
|
+
if (!domain) return "invalid-format";
|
|
486
|
+
const has = await domainHasMail(domain, opts.cache ?? /* @__PURE__ */ new Map(), opts.timeoutMs ?? 5e3);
|
|
487
|
+
return has ? "valid" : "no-mx";
|
|
488
|
+
}
|
|
489
|
+
async function verifyEmails(leads, opts = {}) {
|
|
490
|
+
const cache = /* @__PURE__ */ new Map();
|
|
491
|
+
const targets = leads.filter((l) => l.email);
|
|
492
|
+
const n = Math.max(1, Math.min(opts.concurrency ?? 5, targets.length || 1));
|
|
493
|
+
let next = 0;
|
|
494
|
+
let done = 0;
|
|
495
|
+
const verifyOpts = { cache };
|
|
496
|
+
if (opts.timeoutMs !== void 0) verifyOpts.timeoutMs = opts.timeoutMs;
|
|
497
|
+
const workers = Array.from({ length: n }, async () => {
|
|
498
|
+
for (; ; ) {
|
|
499
|
+
const i = next++;
|
|
500
|
+
if (i >= targets.length) return;
|
|
501
|
+
const lead = targets[i];
|
|
502
|
+
lead.emailStatus = await verifyEmail(lead.email, verifyOpts);
|
|
503
|
+
opts.onProgress?.(`verifying email ${++done}/${targets.length}: ${lead.email}\u2026`);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
await Promise.all(workers);
|
|
507
|
+
return leads;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/export.ts
|
|
511
|
+
var import_csv = require("@lacspace/csv");
|
|
512
|
+
var import_xlsx = require("@lacspace/xlsx");
|
|
513
|
+
var HEADERS = {
|
|
514
|
+
name: "Name",
|
|
515
|
+
category: "Category",
|
|
516
|
+
rating: "Rating",
|
|
517
|
+
reviews: "Reviews",
|
|
518
|
+
priceLevel: "Price",
|
|
519
|
+
address: "Address",
|
|
520
|
+
phone: "Phone",
|
|
521
|
+
website: "Website",
|
|
522
|
+
email: "Email",
|
|
523
|
+
facebook: "Facebook",
|
|
524
|
+
instagram: "Instagram",
|
|
525
|
+
whatsapp: "WhatsApp",
|
|
526
|
+
linkedin: "LinkedIn",
|
|
527
|
+
twitter: "Twitter/X",
|
|
528
|
+
youtube: "YouTube",
|
|
529
|
+
tiktok: "TikTok",
|
|
530
|
+
telegram: "Telegram",
|
|
531
|
+
emailStatus: "Email Status",
|
|
532
|
+
plusCode: "Plus Code",
|
|
533
|
+
latitude: "Latitude",
|
|
534
|
+
longitude: "Longitude",
|
|
535
|
+
hours: "Hours",
|
|
536
|
+
mapsUrl: "Maps URL"
|
|
537
|
+
};
|
|
538
|
+
function toRows(leads, fields = ALL_FIELDS) {
|
|
539
|
+
return leads.map((lead) => {
|
|
540
|
+
const row = {};
|
|
541
|
+
for (const f of fields) {
|
|
542
|
+
const v = lead[f];
|
|
543
|
+
row[HEADERS[f]] = v === void 0 || v === null ? "" : v;
|
|
544
|
+
}
|
|
545
|
+
return row;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function serialize(leads, format, fields = ALL_FIELDS, opts = {}) {
|
|
549
|
+
if (format === "json" || format === "ndjson") {
|
|
550
|
+
const picked = leads.map((lead) => {
|
|
551
|
+
const o = {};
|
|
552
|
+
for (const f of fields) if (lead[f] !== void 0) o[f] = lead[f];
|
|
553
|
+
return o;
|
|
554
|
+
});
|
|
555
|
+
const data = format === "ndjson" ? picked.map((o) => JSON.stringify(o)).join("\n") + (picked.length ? "\n" : "") : JSON.stringify(picked, null, 2);
|
|
556
|
+
return { data, binary: false };
|
|
557
|
+
}
|
|
558
|
+
const rows = toRows(leads, fields);
|
|
559
|
+
if (format === "csv") {
|
|
560
|
+
const csvRows = rows;
|
|
561
|
+
return { data: (0, import_csv.stringify)(csvRows, { escapeFormulas: true }), binary: false };
|
|
562
|
+
}
|
|
563
|
+
const columns = fields.map((f) => {
|
|
564
|
+
const header = HEADERS[f];
|
|
565
|
+
const widest = rows.reduce((m, r) => Math.max(m, String(r[header] ?? "").length), header.length);
|
|
566
|
+
return { header, width: Math.min(60, Math.max(8, widest + 2)) };
|
|
567
|
+
});
|
|
568
|
+
return {
|
|
569
|
+
data: (0, import_xlsx.jsonToXlsx)(rows, { sheetName: opts.sheetName ?? "Leads", columns }),
|
|
570
|
+
binary: true
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function computeStats(leads) {
|
|
574
|
+
const socialKeys = ["facebook", "instagram", "whatsapp", "linkedin", "twitter", "youtube", "tiktok", "telegram"];
|
|
575
|
+
const ratings = leads.map((l) => l.rating).filter((r) => typeof r === "number");
|
|
576
|
+
const stats = {
|
|
577
|
+
total: leads.length,
|
|
578
|
+
withPhone: leads.filter((l) => l.phone).length,
|
|
579
|
+
withWebsite: leads.filter((l) => l.website).length,
|
|
580
|
+
withEmail: leads.filter((l) => l.email).length,
|
|
581
|
+
withValidEmail: leads.filter((l) => l.emailStatus === "valid").length,
|
|
582
|
+
withSocial: leads.filter((l) => socialKeys.some((k) => l[k])).length
|
|
583
|
+
};
|
|
584
|
+
if (ratings.length) stats.avgRating = Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length * 10) / 10;
|
|
585
|
+
return stats;
|
|
586
|
+
}
|
|
587
|
+
var FIELD_BY_KEY = (() => {
|
|
588
|
+
const m = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const f of ALL_FIELDS) {
|
|
590
|
+
m.set(f.toLowerCase(), f);
|
|
591
|
+
m.set(HEADERS[f].toLowerCase(), f);
|
|
592
|
+
}
|
|
593
|
+
return m;
|
|
594
|
+
})();
|
|
595
|
+
var NUMERIC_FIELDS = /* @__PURE__ */ new Set(["rating", "reviews", "latitude", "longitude"]);
|
|
596
|
+
function rowsToLeads(rows) {
|
|
597
|
+
return rows.map((row) => {
|
|
598
|
+
const lead = {};
|
|
599
|
+
for (const [key, raw] of Object.entries(row)) {
|
|
600
|
+
const field = FIELD_BY_KEY.get(key.trim().toLowerCase());
|
|
601
|
+
if (!field || raw === null || raw === void 0 || raw === "") continue;
|
|
602
|
+
if (NUMERIC_FIELDS.has(field)) {
|
|
603
|
+
const n = typeof raw === "number" ? raw : parseFloat(String(raw));
|
|
604
|
+
if (Number.isFinite(n)) lead[field] = n;
|
|
605
|
+
} else {
|
|
606
|
+
lead[field] = String(raw);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return lead;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
241
613
|
// src/scrape.ts
|
|
614
|
+
async function pool(items, n, fn, signal) {
|
|
615
|
+
let next = 0;
|
|
616
|
+
const workers = Array.from({ length: Math.max(1, Math.min(n, items.length)) }, async () => {
|
|
617
|
+
for (; ; ) {
|
|
618
|
+
const i = next++;
|
|
619
|
+
if (i >= items.length || signal?.aborted) return;
|
|
620
|
+
await fn(items[i], i);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
await Promise.all(workers);
|
|
624
|
+
}
|
|
242
625
|
var LeadsError = class extends Error {
|
|
243
626
|
code;
|
|
244
627
|
cause;
|
|
@@ -276,17 +659,19 @@ function parseLatLng(url) {
|
|
|
276
659
|
if (at) return { latitude: parseFloat(at[1]), longitude: parseFloat(at[2]) };
|
|
277
660
|
return {};
|
|
278
661
|
}
|
|
279
|
-
async function launchBrowser(headless) {
|
|
662
|
+
async function launchBrowser(headless, proxy) {
|
|
280
663
|
let lastErr;
|
|
664
|
+
const base = { headless };
|
|
665
|
+
if (proxy) base.proxy = { server: proxy };
|
|
281
666
|
for (const channel of ["chrome", "msedge"]) {
|
|
282
667
|
try {
|
|
283
|
-
return await import_playwright_core.chromium.launch({
|
|
668
|
+
return await import_playwright_core.chromium.launch({ ...base, channel });
|
|
284
669
|
} catch (e) {
|
|
285
670
|
lastErr = e;
|
|
286
671
|
}
|
|
287
672
|
}
|
|
288
673
|
try {
|
|
289
|
-
return await import_playwright_core.chromium.launch(
|
|
674
|
+
return await import_playwright_core.chromium.launch(base);
|
|
290
675
|
} catch (e) {
|
|
291
676
|
lastErr = e;
|
|
292
677
|
}
|
|
@@ -363,7 +748,13 @@ async function extractDetail(page, fields, fallbackName) {
|
|
|
363
748
|
}
|
|
364
749
|
if (fields.has("reviews")) {
|
|
365
750
|
const rv = await box.locator('span[aria-label*="review"]').first().getAttribute("aria-label").catch(() => null);
|
|
366
|
-
|
|
751
|
+
let count = parseReviewCount(rv);
|
|
752
|
+
if (count === void 0) {
|
|
753
|
+
const boxText = await box.innerText().catch(() => "");
|
|
754
|
+
const m = boxText.match(/\(([\d.,\s]+)\)/);
|
|
755
|
+
if (m) count = parseReviewCount(m[1]);
|
|
756
|
+
}
|
|
757
|
+
lead.reviews = count;
|
|
367
758
|
}
|
|
368
759
|
}
|
|
369
760
|
}
|
|
@@ -403,24 +794,31 @@ async function scrapeLeads(opts) {
|
|
|
403
794
|
const fields = new Set(normalizeFields(opts.fields ?? DEFAULT_FIELDS));
|
|
404
795
|
const delayMs = Math.max(0, Math.trunc(opts.delayMs ?? 700));
|
|
405
796
|
const headless = opts.headless ?? false;
|
|
797
|
+
const retries = Math.max(0, Math.trunc(opts.retries ?? 1));
|
|
406
798
|
const onProgress = opts.onProgress;
|
|
799
|
+
const onLead = opts.onLead;
|
|
407
800
|
const signal = opts.signal;
|
|
408
801
|
const startedAt = Date.now();
|
|
409
802
|
const overBudget = () => opts.maxMs !== void 0 && Date.now() - startedAt > opts.maxMs;
|
|
410
|
-
const
|
|
803
|
+
const pauseMs = () => opts.jitter ? Math.round(delayMs * (0.6 + Math.random() * 0.8)) : delayMs;
|
|
804
|
+
const wantVerify = Boolean(opts.verifyEmails) || Boolean(opts.filters?.hasValidEmail) || fields.has("emailStatus");
|
|
805
|
+
const wantEnrich = Boolean(opts.enrich) || ENRICHED_FIELDS.some((f) => fields.has(f)) || Boolean(opts.filters?.hasEmail) || wantVerify;
|
|
411
806
|
const collect = new Set(fields);
|
|
412
807
|
if (wantEnrich) collect.add("website");
|
|
413
808
|
const detailOnly = ["name", "mapsUrl"];
|
|
414
809
|
const wantDetails = (opts.details ?? true) && ([...collect].some((f) => !detailOnly.includes(f)) || wantEnrich);
|
|
415
810
|
onProgress?.(`searching Google Maps for "${query}"\u2026`);
|
|
416
|
-
const browser = await launchBrowser(headless);
|
|
811
|
+
const browser = await launchBrowser(headless, opts.proxy);
|
|
417
812
|
try {
|
|
813
|
+
const locale = opts.locale ?? "en-US";
|
|
418
814
|
const context = await browser.newContext({
|
|
419
815
|
viewport: { width: 1280, height: 900 },
|
|
420
|
-
locale
|
|
816
|
+
locale
|
|
421
817
|
});
|
|
422
818
|
const page = await context.newPage();
|
|
423
|
-
|
|
819
|
+
const urlOpts = { hl: locale };
|
|
820
|
+
if (opts.region) urlOpts.gl = opts.region;
|
|
821
|
+
await page.goto(mapsSearchUrl(query, urlOpts), { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
424
822
|
await dismissConsent(page);
|
|
425
823
|
await loadResults(page, limit, delayMs, onProgress, signal);
|
|
426
824
|
if (signal?.aborted) throw new LeadsError("Search aborted.", "ABORTED");
|
|
@@ -448,44 +846,95 @@ async function scrapeLeads(opts) {
|
|
|
448
846
|
}
|
|
449
847
|
return lead;
|
|
450
848
|
});
|
|
849
|
+
for (const lead of leads) {
|
|
850
|
+
try {
|
|
851
|
+
onLead?.(lead);
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
451
855
|
} else {
|
|
452
856
|
for (let i = 0; i < cards.length; i++) {
|
|
453
857
|
if (signal?.aborted || overBudget()) break;
|
|
454
858
|
const card = cards[i];
|
|
455
859
|
onProgress?.(`reading ${i + 1}/${cards.length}: ${card.name ?? "listing"}\u2026`);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
860
|
+
let lead;
|
|
861
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
862
|
+
try {
|
|
863
|
+
await page.goto(card.href, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
864
|
+
await page.locator("h1.DUwDvf").first().waitFor({ timeout: 8e3 }).catch(() => {
|
|
865
|
+
});
|
|
866
|
+
lead = await extractDetail(page, collect, card.name);
|
|
867
|
+
break;
|
|
868
|
+
} catch {
|
|
869
|
+
if (attempt < retries && !signal?.aborted) {
|
|
870
|
+
onProgress?.(` retrying ${i + 1}/${cards.length} (attempt ${attempt + 2})\u2026`);
|
|
871
|
+
if (delayMs) await page.waitForTimeout(pauseMs());
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (!lead) {
|
|
462
876
|
if (card.name && (collect.has("name") || collect.has("mapsUrl"))) {
|
|
463
|
-
|
|
464
|
-
if (collect.has("name"))
|
|
465
|
-
if (collect.has("mapsUrl"))
|
|
466
|
-
leads.push(partial);
|
|
877
|
+
lead = {};
|
|
878
|
+
if (collect.has("name")) lead.name = card.name;
|
|
879
|
+
if (collect.has("mapsUrl")) lead.mapsUrl = card.href;
|
|
467
880
|
}
|
|
468
881
|
}
|
|
469
|
-
if (
|
|
882
|
+
if (lead) {
|
|
883
|
+
leads.push(lead);
|
|
884
|
+
try {
|
|
885
|
+
onLead?.(lead);
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (delayMs) await page.waitForTimeout(pauseMs());
|
|
470
890
|
}
|
|
471
891
|
}
|
|
472
892
|
onProgress?.(`collected ${leads.length} listing${leads.length === 1 ? "" : "s"}.`);
|
|
893
|
+
if (opts.cleanUrls ?? true) {
|
|
894
|
+
for (const lead of leads) {
|
|
895
|
+
if (lead.website) lead.website = cleanWebsite(lead.website);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
473
898
|
leads = dedupeLeads(leads, opts.dedupe ?? "website");
|
|
474
899
|
if (wantEnrich) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
900
|
+
const socialFields = ENRICHED_FIELDS.filter((f) => f !== "email");
|
|
901
|
+
let done = 0;
|
|
902
|
+
await pool(
|
|
903
|
+
leads,
|
|
904
|
+
Math.max(1, Math.trunc(opts.concurrency ?? 3)),
|
|
905
|
+
async (lead) => {
|
|
906
|
+
if (overBudget()) return;
|
|
907
|
+
const site = lead.website;
|
|
908
|
+
if (site) {
|
|
909
|
+
onProgress?.(`enriching ${++done}/${leads.length}: ${lead.name ?? site}\u2026`);
|
|
910
|
+
const c = await enrichContacts(site);
|
|
911
|
+
if (fields.has("email") && c.email) lead.email = c.email;
|
|
912
|
+
for (const f of socialFields) {
|
|
913
|
+
const v = c[f];
|
|
914
|
+
if (fields.has(f) && v) lead[f] = v;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!fields.has("website")) delete lead.website;
|
|
918
|
+
},
|
|
919
|
+
signal
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
if (wantVerify && !signal?.aborted) {
|
|
923
|
+
onProgress?.("verifying email domains\u2026");
|
|
924
|
+
const vOpts = {
|
|
925
|
+
concurrency: Math.max(1, Math.trunc(opts.concurrency ?? 5))
|
|
926
|
+
};
|
|
927
|
+
if (onProgress) vOpts.onProgress = (m) => onProgress(m);
|
|
928
|
+
await verifyEmails(leads, vOpts);
|
|
929
|
+
}
|
|
930
|
+
if (opts.country && fields.has("phone")) {
|
|
931
|
+
for (const lead of leads) {
|
|
932
|
+
if (lead.phone) lead.phone = normalizePhone(lead.phone, opts.country);
|
|
486
933
|
}
|
|
487
934
|
}
|
|
488
935
|
if (opts.filters) leads = filterLeads(leads, opts.filters);
|
|
936
|
+
if (opts.sort) leads = sortLeads(leads, opts.sort, opts.sortDir);
|
|
937
|
+
if (wantVerify && !fields.has("emailStatus")) for (const l of leads) delete l.emailStatus;
|
|
489
938
|
onProgress?.(`collected ${leads.length} lead${leads.length === 1 ? "" : "s"}.`);
|
|
490
939
|
return leads;
|
|
491
940
|
} finally {
|
|
@@ -493,66 +942,77 @@ async function scrapeLeads(opts) {
|
|
|
493
942
|
});
|
|
494
943
|
}
|
|
495
944
|
}
|
|
945
|
+
async function searchLeadsDetailed(opts) {
|
|
946
|
+
const started = Date.now();
|
|
947
|
+
const leads = await scrapeLeads(opts);
|
|
948
|
+
return { leads, stats: computeStats(leads), elapsedMs: Date.now() - started };
|
|
949
|
+
}
|
|
496
950
|
|
|
497
|
-
// src/
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
951
|
+
// src/batch.ts
|
|
952
|
+
async function searchLeadsBatch(queries, opts = {}) {
|
|
953
|
+
const {
|
|
954
|
+
total,
|
|
955
|
+
onProgress,
|
|
956
|
+
signal,
|
|
957
|
+
type: _t,
|
|
958
|
+
city: _c,
|
|
959
|
+
area: _a,
|
|
960
|
+
query: _q,
|
|
961
|
+
...shared
|
|
962
|
+
} = opts;
|
|
963
|
+
void _t;
|
|
964
|
+
void _c;
|
|
965
|
+
void _a;
|
|
966
|
+
void _q;
|
|
967
|
+
let merged = [];
|
|
968
|
+
const seenDedupe = shared.dedupe ?? "website";
|
|
969
|
+
for (let i = 0; i < queries.length; i++) {
|
|
970
|
+
if (signal?.aborted) break;
|
|
971
|
+
const q = queries[i];
|
|
972
|
+
const label = (() => {
|
|
973
|
+
try {
|
|
974
|
+
return composeQuery(q);
|
|
975
|
+
} catch {
|
|
976
|
+
return q.query ?? q.type ?? "search";
|
|
977
|
+
}
|
|
978
|
+
})();
|
|
979
|
+
onProgress?.(`[${i + 1}/${queries.length}] ${label}`);
|
|
980
|
+
const subOpts = {
|
|
981
|
+
...shared,
|
|
982
|
+
type: q.type ?? "",
|
|
983
|
+
// Defer sorting to the merged pass; keep per-search dedupe/filter local.
|
|
984
|
+
...q.query ? { query: q.query } : {},
|
|
985
|
+
...q.city ? { city: q.city } : {},
|
|
986
|
+
...q.area ? { area: q.area } : {}
|
|
987
|
+
};
|
|
988
|
+
delete subOpts.sort;
|
|
989
|
+
if (signal) subOpts.signal = signal;
|
|
990
|
+
if (onProgress) subOpts.onProgress = (m) => onProgress(` ${m}`);
|
|
991
|
+
try {
|
|
992
|
+
const found = await scrapeLeads(subOpts);
|
|
993
|
+
merged = merged.concat(found);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
onProgress?.(` ! skipped "${label}": ${err.message}`);
|
|
525
996
|
}
|
|
526
|
-
return row;
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
function serialize(leads, format, fields = ALL_FIELDS, opts = {}) {
|
|
530
|
-
if (format === "json") {
|
|
531
|
-
const picked = leads.map((lead) => {
|
|
532
|
-
const o = {};
|
|
533
|
-
for (const f of fields) if (lead[f] !== void 0) o[f] = lead[f];
|
|
534
|
-
return o;
|
|
535
|
-
});
|
|
536
|
-
return { data: JSON.stringify(picked, null, 2), binary: false };
|
|
537
|
-
}
|
|
538
|
-
const rows = toRows(leads, fields);
|
|
539
|
-
if (format === "csv") {
|
|
540
|
-
const csvRows = rows;
|
|
541
|
-
return { data: (0, import_csv.stringify)(csvRows, { escapeFormulas: true }), binary: false };
|
|
542
997
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
998
|
+
merged = dedupeLeads(merged, seenDedupe);
|
|
999
|
+
if (opts.sort) merged = sortLeads(merged, opts.sort, opts.sortDir);
|
|
1000
|
+
if (total !== void 0 && total > 0) merged = merged.slice(0, total);
|
|
1001
|
+
onProgress?.(`merged ${merged.length} unique lead${merged.length === 1 ? "" : "s"}.`);
|
|
1002
|
+
return merged;
|
|
1003
|
+
}
|
|
1004
|
+
async function searchLeadsMulti(opts) {
|
|
1005
|
+
const queries = expandQueries({
|
|
1006
|
+
...opts.type ? { type: opts.type } : {},
|
|
1007
|
+
...opts.city ? { city: opts.city } : {},
|
|
1008
|
+
...opts.area ? { area: opts.area } : {},
|
|
1009
|
+
...opts.query ? { query: opts.query } : {}
|
|
547
1010
|
});
|
|
548
|
-
return
|
|
549
|
-
data: (0, import_xlsx.jsonToXlsx)(rows, { sheetName: opts.sheetName ?? "Leads", columns }),
|
|
550
|
-
binary: true
|
|
551
|
-
};
|
|
1011
|
+
return searchLeadsBatch(queries, opts);
|
|
552
1012
|
}
|
|
553
1013
|
|
|
554
1014
|
// src/convert.ts
|
|
555
|
-
var
|
|
1015
|
+
var import_promises2 = require("fs/promises");
|
|
556
1016
|
var import_node_path = require("path");
|
|
557
1017
|
var import_csv2 = require("@lacspace/csv");
|
|
558
1018
|
var import_xlsx2 = require("@lacspace/xlsx");
|
|
@@ -566,10 +1026,10 @@ function detectFormat(file) {
|
|
|
566
1026
|
async function readRows(file, format) {
|
|
567
1027
|
const fmt = format ?? detectFormat(file);
|
|
568
1028
|
if (fmt === "xlsx") {
|
|
569
|
-
const bytes = await (0,
|
|
1029
|
+
const bytes = await (0, import_promises2.readFile)(file);
|
|
570
1030
|
return await (0, import_xlsx2.xlsxToJson)(bytes);
|
|
571
1031
|
}
|
|
572
|
-
const text = await (0,
|
|
1032
|
+
const text = await (0, import_promises2.readFile)(file, "utf8");
|
|
573
1033
|
if (fmt === "csv") {
|
|
574
1034
|
return (0, import_csv2.parse)(text, { header: true });
|
|
575
1035
|
}
|
|
@@ -621,33 +1081,51 @@ async function convertFile(input, opts = {}) {
|
|
|
621
1081
|
const format = opts.format ?? (opts.out ? detectFormat(opts.out) : "json");
|
|
622
1082
|
const out = (0, import_node_path.resolve)(opts.out ?? input.replace(/\.[^.]+$/, "") + "." + format);
|
|
623
1083
|
const { data, binary } = serializeRows(rows, format, { sheetName: opts.sheetName ?? "Sheet1" });
|
|
624
|
-
await (0,
|
|
1084
|
+
await (0, import_promises2.writeFile)(out, binary ? Buffer.from(data) : data);
|
|
625
1085
|
return { out, format, count: rows.length };
|
|
626
1086
|
}
|
|
627
1087
|
// Annotate the CommonJS export names for ESM import in node:
|
|
628
1088
|
0 && (module.exports = {
|
|
629
1089
|
ALL_FIELDS,
|
|
1090
|
+
CALLING_CODES,
|
|
1091
|
+
DEFAULT_FIELDS,
|
|
630
1092
|
ENRICHED_FIELDS,
|
|
1093
|
+
FIELD_PRESETS,
|
|
631
1094
|
LeadsError,
|
|
1095
|
+
callingCode,
|
|
1096
|
+
cleanWebsite,
|
|
632
1097
|
columnsOf,
|
|
633
1098
|
composeQuery,
|
|
1099
|
+
computeStats,
|
|
634
1100
|
convertFile,
|
|
635
1101
|
dedupeLeads,
|
|
636
1102
|
defaultFilename,
|
|
637
1103
|
detectFormat,
|
|
1104
|
+
emailDomain,
|
|
1105
|
+
emailFormatValid,
|
|
638
1106
|
enrichContacts,
|
|
1107
|
+
expandQueries,
|
|
639
1108
|
extractEmails,
|
|
640
1109
|
extractSocials,
|
|
641
1110
|
filterLeads,
|
|
642
1111
|
mapsSearchUrl,
|
|
643
1112
|
normalizeFields,
|
|
1113
|
+
normalizePhone,
|
|
644
1114
|
parseLatLng,
|
|
645
1115
|
parseRating,
|
|
646
1116
|
parseReviewCount,
|
|
647
1117
|
readRows,
|
|
1118
|
+
resolvePreset,
|
|
1119
|
+
rowsToLeads,
|
|
648
1120
|
scrapeLeads,
|
|
649
1121
|
searchLeads,
|
|
1122
|
+
searchLeadsBatch,
|
|
1123
|
+
searchLeadsDetailed,
|
|
1124
|
+
searchLeadsMulti,
|
|
650
1125
|
serialize,
|
|
651
1126
|
serializeRows,
|
|
652
|
-
|
|
1127
|
+
sortLeads,
|
|
1128
|
+
toRows,
|
|
1129
|
+
verifyEmail,
|
|
1130
|
+
verifyEmails
|
|
653
1131
|
});
|