lacspace-leads 0.3.0 → 1.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 +50 -8
- package/dist/cli.js +383 -100
- package/dist/lib.cjs +323 -91
- package/dist/lib.d.cts +134 -8
- package/dist/lib.d.ts +134 -8
- package/dist/lib.js +307 -86
- package/package.json +6 -2
package/dist/lib.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The fields a {@link Lead} can carry — request any subset via `fields`. */
|
|
2
|
-
type LeadField = "name" | "category" | "rating" | "reviews" | "priceLevel" | "address" | "phone" | "website" | "email" | "facebook" | "instagram" | "whatsapp" | "linkedin" | "twitter" | "youtube" | "tiktok" | "telegram" | "plusCode" | "latitude" | "longitude" | "hours" | "mapsUrl";
|
|
2
|
+
type LeadField = "name" | "category" | "rating" | "reviews" | "priceLevel" | "address" | "phone" | "website" | "email" | "facebook" | "instagram" | "whatsapp" | "linkedin" | "twitter" | "youtube" | "tiktok" | "telegram" | "emailStatus" | "plusCode" | "latitude" | "longitude" | "distanceKm" | "hours" | "mapsUrl";
|
|
3
|
+
/** Deliverability verdict for a discovered email (see `verifyEmails`). */
|
|
4
|
+
type EmailStatus = "valid" | "no-mx" | "invalid-format" | "unknown";
|
|
3
5
|
/** Every field, in a sensible column order for exports. */
|
|
4
6
|
declare const ALL_FIELDS: LeadField[];
|
|
5
7
|
/** Fields that require visiting the business website (via `enrich`). */
|
|
@@ -43,12 +45,16 @@ interface Lead {
|
|
|
43
45
|
tiktok?: string;
|
|
44
46
|
/** Telegram link, from the website (needs enrichment). */
|
|
45
47
|
telegram?: string;
|
|
48
|
+
/** Email deliverability verdict (set by `verifyEmails`). */
|
|
49
|
+
emailStatus?: EmailStatus;
|
|
46
50
|
/** Google Plus Code, when shown. */
|
|
47
51
|
plusCode?: string;
|
|
48
52
|
/** Latitude, parsed from the listing's Maps URL. */
|
|
49
53
|
latitude?: number;
|
|
50
54
|
/** Longitude, parsed from the listing's Maps URL. */
|
|
51
55
|
longitude?: number;
|
|
56
|
+
/** Distance in km from the `near` point (set only in radius search). */
|
|
57
|
+
distanceKm?: number;
|
|
52
58
|
/** Opening-hours summary, when shown. */
|
|
53
59
|
hours?: string;
|
|
54
60
|
/** Canonical Google Maps URL for the listing. */
|
|
@@ -66,11 +72,13 @@ interface LeadFilters {
|
|
|
66
72
|
hasWebsite?: boolean;
|
|
67
73
|
/** Keep only leads that have an email (implies enrichment). */
|
|
68
74
|
hasEmail?: boolean;
|
|
75
|
+
/** Keep only leads whose email passed MX verification (implies `verifyEmails`). */
|
|
76
|
+
hasValidEmail?: boolean;
|
|
69
77
|
}
|
|
70
78
|
/** Output formats the tool can write. */
|
|
71
79
|
type OutputFormat = "json" | "ndjson" | "csv" | "xlsx";
|
|
72
80
|
/** How to sort collected leads before export. */
|
|
73
|
-
type SortKey$1 = "rating" | "reviews" | "name" | "priceLevel";
|
|
81
|
+
type SortKey$1 = "rating" | "reviews" | "name" | "priceLevel" | "distance";
|
|
74
82
|
/** Options for a lead search. */
|
|
75
83
|
interface SearchOptions {
|
|
76
84
|
/** City, e.g. "Kathmandu". */
|
|
@@ -81,6 +89,16 @@ interface SearchOptions {
|
|
|
81
89
|
type: string;
|
|
82
90
|
/** A ready-made query, used verbatim instead of composing city/area/type. */
|
|
83
91
|
query?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Centre the search on a coordinate. Results are biased to this point, and —
|
|
94
|
+
* when `radiusM` is set — filtered to within it. Each lead gets `distanceKm`.
|
|
95
|
+
*/
|
|
96
|
+
near?: {
|
|
97
|
+
lat: number;
|
|
98
|
+
lng: number;
|
|
99
|
+
};
|
|
100
|
+
/** Keep only leads within this many metres of `near`. Requires `near`. */
|
|
101
|
+
radiusM?: number;
|
|
84
102
|
/** Max number of leads to collect. Default 60. */
|
|
85
103
|
limit?: number;
|
|
86
104
|
/** Fields to collect. Default: all of {@link ALL_FIELDS}. */
|
|
@@ -103,8 +121,12 @@ interface SearchOptions {
|
|
|
103
121
|
enrich?: boolean;
|
|
104
122
|
/** Drop leads that don't pass these filters. */
|
|
105
123
|
filters?: LeadFilters;
|
|
106
|
-
/**
|
|
107
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Drop duplicate leads by this key. `"smart"` uses the strongest identity
|
|
126
|
+
* available per lead (website → phone → name) and is best for accumulating
|
|
127
|
+
* across runs. Default "website".
|
|
128
|
+
*/
|
|
129
|
+
dedupe?: "website" | "phone" | "name" | "smart" | "none";
|
|
108
130
|
/** Sort the results by this key before returning. */
|
|
109
131
|
sort?: SortKey$1;
|
|
110
132
|
/** Sort direction. Defaults to "desc" for numbers, "asc" for name. */
|
|
@@ -122,17 +144,43 @@ interface SearchOptions {
|
|
|
122
144
|
cleanUrls?: boolean;
|
|
123
145
|
/** How many websites to enrich in parallel. Default 3. */
|
|
124
146
|
concurrency?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Verify each discovered email by checking its domain has MX records (DNS,
|
|
149
|
+
* no message is sent). Populates `emailStatus`. Implies `enrich`. Default off.
|
|
150
|
+
*/
|
|
151
|
+
verifyEmails?: boolean;
|
|
125
152
|
/** Browser/UI locale, e.g. "en-US", "ne-NP". Default "en-US". */
|
|
126
153
|
locale?: string;
|
|
127
154
|
/** Google region bias (ccTLD-style), e.g. "np", "us". Sets Maps `gl`. */
|
|
128
155
|
region?: string;
|
|
156
|
+
/** Route the browser through this proxy, e.g. "http://user:pass@host:port". */
|
|
157
|
+
proxy?: string;
|
|
158
|
+
/** Retry a listing that fails to open, up to this many times. Default 1. */
|
|
159
|
+
retries?: number;
|
|
160
|
+
/** Randomise the delay between listings by ±40% to look more human. */
|
|
161
|
+
jitter?: boolean;
|
|
129
162
|
/** Stop collecting after this many milliseconds (best-effort). */
|
|
130
163
|
maxMs?: number;
|
|
131
164
|
/** Called with a short progress message as the search runs. */
|
|
132
165
|
onProgress?: (message: string) => void;
|
|
166
|
+
/**
|
|
167
|
+
* Called with each lead as it's collected (before enrichment), for live UIs
|
|
168
|
+
* or crash-safe incremental writing. Never throws into the run.
|
|
169
|
+
*/
|
|
170
|
+
onLead?: (lead: Lead) => void;
|
|
133
171
|
/** An AbortSignal to cancel a running search. */
|
|
134
172
|
signal?: AbortSignal;
|
|
135
173
|
}
|
|
174
|
+
/** Aggregate counts over a lead list — see `computeStats`. */
|
|
175
|
+
interface LeadStats {
|
|
176
|
+
total: number;
|
|
177
|
+
withPhone: number;
|
|
178
|
+
withWebsite: number;
|
|
179
|
+
withEmail: number;
|
|
180
|
+
withValidEmail: number;
|
|
181
|
+
withSocial: number;
|
|
182
|
+
avgRating?: number;
|
|
183
|
+
}
|
|
136
184
|
|
|
137
185
|
/** Error thrown by the scraper. Carries a machine `code` and optional cause. */
|
|
138
186
|
declare class LeadsError extends Error {
|
|
@@ -162,6 +210,18 @@ declare function parseLatLng(url: string): {
|
|
|
162
210
|
* @throws {LeadsError} when no browser can be launched, or the search is aborted.
|
|
163
211
|
*/
|
|
164
212
|
declare function scrapeLeads(opts: SearchOptions): Promise<Lead[]>;
|
|
213
|
+
/** A search result with aggregate stats and timing. */
|
|
214
|
+
interface DetailedResult {
|
|
215
|
+
leads: Lead[];
|
|
216
|
+
stats: LeadStats;
|
|
217
|
+
/** Wall-clock time of the search, in milliseconds. */
|
|
218
|
+
elapsedMs: number;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Like {@link scrapeLeads}, but also returns aggregate {@link LeadStats} and the
|
|
222
|
+
* elapsed time — handy for dashboards, reports and CI summaries.
|
|
223
|
+
*/
|
|
224
|
+
declare function searchLeadsDetailed(opts: SearchOptions): Promise<DetailedResult>;
|
|
165
225
|
|
|
166
226
|
/** One sub-search in a batch. */
|
|
167
227
|
interface BatchQuery {
|
|
@@ -198,6 +258,37 @@ declare function serialize(leads: Lead[], format: OutputFormat, fields?: LeadFie
|
|
|
198
258
|
data: string | Uint8Array;
|
|
199
259
|
binary: boolean;
|
|
200
260
|
};
|
|
261
|
+
/** Aggregate counts over a lead list — for summaries and reports. Pure. */
|
|
262
|
+
declare function computeStats(leads: Lead[]): LeadStats;
|
|
263
|
+
/**
|
|
264
|
+
* Map arbitrary rows (as read back from a JSON/CSV/Excel export, keyed by field
|
|
265
|
+
* name or header label) into {@link Lead}s. Unknown columns are ignored, blanks
|
|
266
|
+
* dropped, and numeric fields coerced. Powers `--append` / resume. Pure.
|
|
267
|
+
*/
|
|
268
|
+
declare function rowsToLeads(rows: Record<string, unknown>[]): Lead[];
|
|
269
|
+
|
|
270
|
+
/** True when `email` is a plausibly-valid address shape. Pure. */
|
|
271
|
+
declare function emailFormatValid(email: string | undefined): boolean;
|
|
272
|
+
/** The domain part of an email, lower-cased, or undefined. Pure. */
|
|
273
|
+
declare function emailDomain(email: string | undefined): string | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* Classify a single email: `invalid-format` → `no-mx` → `valid`. `unknown` is
|
|
276
|
+
* reserved for a missing address. Never throws.
|
|
277
|
+
*/
|
|
278
|
+
declare function verifyEmail(email: string | undefined, opts?: {
|
|
279
|
+
cache?: Map<string, boolean>;
|
|
280
|
+
timeoutMs?: number;
|
|
281
|
+
}): Promise<EmailStatus>;
|
|
282
|
+
/**
|
|
283
|
+
* Verify the `email` on each lead in place, setting `emailStatus`. Domains are
|
|
284
|
+
* looked up once (shared cache) and checked a few in parallel. Leads without an
|
|
285
|
+
* email are left untouched. Node-only; never throws.
|
|
286
|
+
*/
|
|
287
|
+
declare function verifyEmails(leads: Lead[], opts?: {
|
|
288
|
+
concurrency?: number;
|
|
289
|
+
timeoutMs?: number;
|
|
290
|
+
onProgress?: (m: string) => void;
|
|
291
|
+
}): Promise<Lead[]>;
|
|
201
292
|
|
|
202
293
|
/**
|
|
203
294
|
* Compose the human search query from options: an explicit `query` wins,
|
|
@@ -211,11 +302,17 @@ declare function composeQuery(opts: {
|
|
|
211
302
|
}): string;
|
|
212
303
|
/**
|
|
213
304
|
* The Google Maps search URL for a query. `hl` (interface language) keeps the
|
|
214
|
-
* scraped aria-labels predictable; `gl` biases results to a region
|
|
305
|
+
* scraped aria-labels predictable; `gl` biases results to a region; `center`
|
|
306
|
+
* (`{lat,lng,zoom}`) points the map at a location for a radius search.
|
|
215
307
|
*/
|
|
216
308
|
declare function mapsSearchUrl(query: string, opts?: {
|
|
217
309
|
hl?: string;
|
|
218
310
|
gl?: string;
|
|
311
|
+
center?: {
|
|
312
|
+
lat: number;
|
|
313
|
+
lng: number;
|
|
314
|
+
zoom?: number;
|
|
315
|
+
};
|
|
219
316
|
}): string;
|
|
220
317
|
/**
|
|
221
318
|
* Expand a possibly-multi search request into individual `{type, city, area}`
|
|
@@ -280,10 +377,11 @@ declare function callingCode(country?: string): string | undefined;
|
|
|
280
377
|
*/
|
|
281
378
|
declare function normalizePhone(raw?: string, country?: string): string | undefined;
|
|
282
379
|
/** Keys a lead list can be sorted by. */
|
|
283
|
-
type SortKey = "rating" | "reviews" | "name" | "priceLevel";
|
|
380
|
+
type SortKey = "rating" | "reviews" | "name" | "priceLevel" | "distance";
|
|
284
381
|
/**
|
|
285
382
|
* Sort leads by a key, missing values always last. Stable, pure, new array.
|
|
286
|
-
* `dir` defaults to descending for numeric keys
|
|
383
|
+
* `dir` defaults to descending for numeric quality keys, ascending for `name`
|
|
384
|
+
* and `distance` (nearest first).
|
|
287
385
|
*/
|
|
288
386
|
declare function sortLeads(leads: Lead[], by?: SortKey, dir?: "asc" | "desc"): Lead[];
|
|
289
387
|
|
|
@@ -320,6 +418,34 @@ declare function enrichContacts(website: string, opts?: {
|
|
|
320
418
|
contactPages?: boolean;
|
|
321
419
|
}): Promise<Contacts>;
|
|
322
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Geo helpers for radius search — parse a coordinate/radius, measure distance
|
|
423
|
+
* (haversine), and pick a Maps zoom that frames a radius. All pure, unit-tested.
|
|
424
|
+
*/
|
|
425
|
+
/** A latitude/longitude point. */
|
|
426
|
+
interface LatLng {
|
|
427
|
+
lat: number;
|
|
428
|
+
lng: number;
|
|
429
|
+
}
|
|
430
|
+
/** Great-circle distance between two points, in metres. Pure. */
|
|
431
|
+
declare function haversineMeters(a: LatLng, b: LatLng): number;
|
|
432
|
+
/**
|
|
433
|
+
* Parse a `"lat,lng"` pair (also accepts a bare `@lat,lng` or whitespace).
|
|
434
|
+
* Returns `undefined` if it isn't two in-range numbers. Pure.
|
|
435
|
+
*/
|
|
436
|
+
declare function parseLatLngPair(input: string | undefined): LatLng | undefined;
|
|
437
|
+
/**
|
|
438
|
+
* Parse a distance like `"2km"`, `"500m"`, `"1.5mi"` (or a bare number = metres)
|
|
439
|
+
* into metres. Returns `undefined` for junk or non-positive values. Pure.
|
|
440
|
+
*/
|
|
441
|
+
declare function parseDistance(input: string | undefined): number | undefined;
|
|
442
|
+
/**
|
|
443
|
+
* A Google Maps zoom level (integer) that roughly frames a circle of `radiusM`
|
|
444
|
+
* at a given latitude in the map viewport. Clamped to 3–19. Pure/best-effort —
|
|
445
|
+
* Maps re-fits results anyway; this just centres the search sensibly.
|
|
446
|
+
*/
|
|
447
|
+
declare function zoomForRadius(radiusM: number, lat?: number): number;
|
|
448
|
+
|
|
323
449
|
/** An arbitrary tabular row. */
|
|
324
450
|
type DataRow = Record<string, unknown>;
|
|
325
451
|
/** Infer the format from a filename extension. Throws on an unknown extension. */
|
|
@@ -346,4 +472,4 @@ declare function convertFile(input: string, opts?: {
|
|
|
346
472
|
count: number;
|
|
347
473
|
}>;
|
|
348
474
|
|
|
349
|
-
export { ALL_FIELDS, type BatchQuery, CALLING_CODES, type Contacts, DEFAULT_FIELDS, type DataRow, ENRICHED_FIELDS, FIELD_PRESETS, type Lead, type LeadField, type LeadFilters, LeadsError, type OutputFormat, type SearchOptions, type SortKey$1 as SortKey, callingCode, cleanWebsite, columnsOf, composeQuery, convertFile, dedupeLeads, defaultFilename, detectFormat, enrichContacts, expandQueries, extractEmails, extractSocials, filterLeads, mapsSearchUrl, normalizeFields, normalizePhone, parseLatLng, parseRating, parseReviewCount, readRows, resolvePreset, scrapeLeads, scrapeLeads as searchLeads, searchLeadsBatch, searchLeadsMulti, serialize, serializeRows, sortLeads, toRows };
|
|
475
|
+
export { ALL_FIELDS, type BatchQuery, CALLING_CODES, type Contacts, DEFAULT_FIELDS, type DataRow, type DetailedResult, ENRICHED_FIELDS, type EmailStatus, FIELD_PRESETS, type LatLng, type Lead, type LeadField, type LeadFilters, type LeadStats, LeadsError, type OutputFormat, type SearchOptions, type SortKey$1 as SortKey, callingCode, cleanWebsite, columnsOf, composeQuery, computeStats, convertFile, dedupeLeads, defaultFilename, detectFormat, emailDomain, emailFormatValid, enrichContacts, expandQueries, extractEmails, extractSocials, filterLeads, haversineMeters, mapsSearchUrl, normalizeFields, normalizePhone, parseDistance, parseLatLng, parseLatLngPair, parseRating, parseReviewCount, readRows, resolvePreset, rowsToLeads, scrapeLeads, scrapeLeads as searchLeads, searchLeadsBatch, searchLeadsDetailed, searchLeadsMulti, serialize, serializeRows, sortLeads, toRows, verifyEmail, verifyEmails, zoomForRadius };
|
package/dist/lib.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The fields a {@link Lead} can carry — request any subset via `fields`. */
|
|
2
|
-
type LeadField = "name" | "category" | "rating" | "reviews" | "priceLevel" | "address" | "phone" | "website" | "email" | "facebook" | "instagram" | "whatsapp" | "linkedin" | "twitter" | "youtube" | "tiktok" | "telegram" | "plusCode" | "latitude" | "longitude" | "hours" | "mapsUrl";
|
|
2
|
+
type LeadField = "name" | "category" | "rating" | "reviews" | "priceLevel" | "address" | "phone" | "website" | "email" | "facebook" | "instagram" | "whatsapp" | "linkedin" | "twitter" | "youtube" | "tiktok" | "telegram" | "emailStatus" | "plusCode" | "latitude" | "longitude" | "distanceKm" | "hours" | "mapsUrl";
|
|
3
|
+
/** Deliverability verdict for a discovered email (see `verifyEmails`). */
|
|
4
|
+
type EmailStatus = "valid" | "no-mx" | "invalid-format" | "unknown";
|
|
3
5
|
/** Every field, in a sensible column order for exports. */
|
|
4
6
|
declare const ALL_FIELDS: LeadField[];
|
|
5
7
|
/** Fields that require visiting the business website (via `enrich`). */
|
|
@@ -43,12 +45,16 @@ interface Lead {
|
|
|
43
45
|
tiktok?: string;
|
|
44
46
|
/** Telegram link, from the website (needs enrichment). */
|
|
45
47
|
telegram?: string;
|
|
48
|
+
/** Email deliverability verdict (set by `verifyEmails`). */
|
|
49
|
+
emailStatus?: EmailStatus;
|
|
46
50
|
/** Google Plus Code, when shown. */
|
|
47
51
|
plusCode?: string;
|
|
48
52
|
/** Latitude, parsed from the listing's Maps URL. */
|
|
49
53
|
latitude?: number;
|
|
50
54
|
/** Longitude, parsed from the listing's Maps URL. */
|
|
51
55
|
longitude?: number;
|
|
56
|
+
/** Distance in km from the `near` point (set only in radius search). */
|
|
57
|
+
distanceKm?: number;
|
|
52
58
|
/** Opening-hours summary, when shown. */
|
|
53
59
|
hours?: string;
|
|
54
60
|
/** Canonical Google Maps URL for the listing. */
|
|
@@ -66,11 +72,13 @@ interface LeadFilters {
|
|
|
66
72
|
hasWebsite?: boolean;
|
|
67
73
|
/** Keep only leads that have an email (implies enrichment). */
|
|
68
74
|
hasEmail?: boolean;
|
|
75
|
+
/** Keep only leads whose email passed MX verification (implies `verifyEmails`). */
|
|
76
|
+
hasValidEmail?: boolean;
|
|
69
77
|
}
|
|
70
78
|
/** Output formats the tool can write. */
|
|
71
79
|
type OutputFormat = "json" | "ndjson" | "csv" | "xlsx";
|
|
72
80
|
/** How to sort collected leads before export. */
|
|
73
|
-
type SortKey$1 = "rating" | "reviews" | "name" | "priceLevel";
|
|
81
|
+
type SortKey$1 = "rating" | "reviews" | "name" | "priceLevel" | "distance";
|
|
74
82
|
/** Options for a lead search. */
|
|
75
83
|
interface SearchOptions {
|
|
76
84
|
/** City, e.g. "Kathmandu". */
|
|
@@ -81,6 +89,16 @@ interface SearchOptions {
|
|
|
81
89
|
type: string;
|
|
82
90
|
/** A ready-made query, used verbatim instead of composing city/area/type. */
|
|
83
91
|
query?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Centre the search on a coordinate. Results are biased to this point, and —
|
|
94
|
+
* when `radiusM` is set — filtered to within it. Each lead gets `distanceKm`.
|
|
95
|
+
*/
|
|
96
|
+
near?: {
|
|
97
|
+
lat: number;
|
|
98
|
+
lng: number;
|
|
99
|
+
};
|
|
100
|
+
/** Keep only leads within this many metres of `near`. Requires `near`. */
|
|
101
|
+
radiusM?: number;
|
|
84
102
|
/** Max number of leads to collect. Default 60. */
|
|
85
103
|
limit?: number;
|
|
86
104
|
/** Fields to collect. Default: all of {@link ALL_FIELDS}. */
|
|
@@ -103,8 +121,12 @@ interface SearchOptions {
|
|
|
103
121
|
enrich?: boolean;
|
|
104
122
|
/** Drop leads that don't pass these filters. */
|
|
105
123
|
filters?: LeadFilters;
|
|
106
|
-
/**
|
|
107
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Drop duplicate leads by this key. `"smart"` uses the strongest identity
|
|
126
|
+
* available per lead (website → phone → name) and is best for accumulating
|
|
127
|
+
* across runs. Default "website".
|
|
128
|
+
*/
|
|
129
|
+
dedupe?: "website" | "phone" | "name" | "smart" | "none";
|
|
108
130
|
/** Sort the results by this key before returning. */
|
|
109
131
|
sort?: SortKey$1;
|
|
110
132
|
/** Sort direction. Defaults to "desc" for numbers, "asc" for name. */
|
|
@@ -122,17 +144,43 @@ interface SearchOptions {
|
|
|
122
144
|
cleanUrls?: boolean;
|
|
123
145
|
/** How many websites to enrich in parallel. Default 3. */
|
|
124
146
|
concurrency?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Verify each discovered email by checking its domain has MX records (DNS,
|
|
149
|
+
* no message is sent). Populates `emailStatus`. Implies `enrich`. Default off.
|
|
150
|
+
*/
|
|
151
|
+
verifyEmails?: boolean;
|
|
125
152
|
/** Browser/UI locale, e.g. "en-US", "ne-NP". Default "en-US". */
|
|
126
153
|
locale?: string;
|
|
127
154
|
/** Google region bias (ccTLD-style), e.g. "np", "us". Sets Maps `gl`. */
|
|
128
155
|
region?: string;
|
|
156
|
+
/** Route the browser through this proxy, e.g. "http://user:pass@host:port". */
|
|
157
|
+
proxy?: string;
|
|
158
|
+
/** Retry a listing that fails to open, up to this many times. Default 1. */
|
|
159
|
+
retries?: number;
|
|
160
|
+
/** Randomise the delay between listings by ±40% to look more human. */
|
|
161
|
+
jitter?: boolean;
|
|
129
162
|
/** Stop collecting after this many milliseconds (best-effort). */
|
|
130
163
|
maxMs?: number;
|
|
131
164
|
/** Called with a short progress message as the search runs. */
|
|
132
165
|
onProgress?: (message: string) => void;
|
|
166
|
+
/**
|
|
167
|
+
* Called with each lead as it's collected (before enrichment), for live UIs
|
|
168
|
+
* or crash-safe incremental writing. Never throws into the run.
|
|
169
|
+
*/
|
|
170
|
+
onLead?: (lead: Lead) => void;
|
|
133
171
|
/** An AbortSignal to cancel a running search. */
|
|
134
172
|
signal?: AbortSignal;
|
|
135
173
|
}
|
|
174
|
+
/** Aggregate counts over a lead list — see `computeStats`. */
|
|
175
|
+
interface LeadStats {
|
|
176
|
+
total: number;
|
|
177
|
+
withPhone: number;
|
|
178
|
+
withWebsite: number;
|
|
179
|
+
withEmail: number;
|
|
180
|
+
withValidEmail: number;
|
|
181
|
+
withSocial: number;
|
|
182
|
+
avgRating?: number;
|
|
183
|
+
}
|
|
136
184
|
|
|
137
185
|
/** Error thrown by the scraper. Carries a machine `code` and optional cause. */
|
|
138
186
|
declare class LeadsError extends Error {
|
|
@@ -162,6 +210,18 @@ declare function parseLatLng(url: string): {
|
|
|
162
210
|
* @throws {LeadsError} when no browser can be launched, or the search is aborted.
|
|
163
211
|
*/
|
|
164
212
|
declare function scrapeLeads(opts: SearchOptions): Promise<Lead[]>;
|
|
213
|
+
/** A search result with aggregate stats and timing. */
|
|
214
|
+
interface DetailedResult {
|
|
215
|
+
leads: Lead[];
|
|
216
|
+
stats: LeadStats;
|
|
217
|
+
/** Wall-clock time of the search, in milliseconds. */
|
|
218
|
+
elapsedMs: number;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Like {@link scrapeLeads}, but also returns aggregate {@link LeadStats} and the
|
|
222
|
+
* elapsed time — handy for dashboards, reports and CI summaries.
|
|
223
|
+
*/
|
|
224
|
+
declare function searchLeadsDetailed(opts: SearchOptions): Promise<DetailedResult>;
|
|
165
225
|
|
|
166
226
|
/** One sub-search in a batch. */
|
|
167
227
|
interface BatchQuery {
|
|
@@ -198,6 +258,37 @@ declare function serialize(leads: Lead[], format: OutputFormat, fields?: LeadFie
|
|
|
198
258
|
data: string | Uint8Array;
|
|
199
259
|
binary: boolean;
|
|
200
260
|
};
|
|
261
|
+
/** Aggregate counts over a lead list — for summaries and reports. Pure. */
|
|
262
|
+
declare function computeStats(leads: Lead[]): LeadStats;
|
|
263
|
+
/**
|
|
264
|
+
* Map arbitrary rows (as read back from a JSON/CSV/Excel export, keyed by field
|
|
265
|
+
* name or header label) into {@link Lead}s. Unknown columns are ignored, blanks
|
|
266
|
+
* dropped, and numeric fields coerced. Powers `--append` / resume. Pure.
|
|
267
|
+
*/
|
|
268
|
+
declare function rowsToLeads(rows: Record<string, unknown>[]): Lead[];
|
|
269
|
+
|
|
270
|
+
/** True when `email` is a plausibly-valid address shape. Pure. */
|
|
271
|
+
declare function emailFormatValid(email: string | undefined): boolean;
|
|
272
|
+
/** The domain part of an email, lower-cased, or undefined. Pure. */
|
|
273
|
+
declare function emailDomain(email: string | undefined): string | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* Classify a single email: `invalid-format` → `no-mx` → `valid`. `unknown` is
|
|
276
|
+
* reserved for a missing address. Never throws.
|
|
277
|
+
*/
|
|
278
|
+
declare function verifyEmail(email: string | undefined, opts?: {
|
|
279
|
+
cache?: Map<string, boolean>;
|
|
280
|
+
timeoutMs?: number;
|
|
281
|
+
}): Promise<EmailStatus>;
|
|
282
|
+
/**
|
|
283
|
+
* Verify the `email` on each lead in place, setting `emailStatus`. Domains are
|
|
284
|
+
* looked up once (shared cache) and checked a few in parallel. Leads without an
|
|
285
|
+
* email are left untouched. Node-only; never throws.
|
|
286
|
+
*/
|
|
287
|
+
declare function verifyEmails(leads: Lead[], opts?: {
|
|
288
|
+
concurrency?: number;
|
|
289
|
+
timeoutMs?: number;
|
|
290
|
+
onProgress?: (m: string) => void;
|
|
291
|
+
}): Promise<Lead[]>;
|
|
201
292
|
|
|
202
293
|
/**
|
|
203
294
|
* Compose the human search query from options: an explicit `query` wins,
|
|
@@ -211,11 +302,17 @@ declare function composeQuery(opts: {
|
|
|
211
302
|
}): string;
|
|
212
303
|
/**
|
|
213
304
|
* The Google Maps search URL for a query. `hl` (interface language) keeps the
|
|
214
|
-
* scraped aria-labels predictable; `gl` biases results to a region
|
|
305
|
+
* scraped aria-labels predictable; `gl` biases results to a region; `center`
|
|
306
|
+
* (`{lat,lng,zoom}`) points the map at a location for a radius search.
|
|
215
307
|
*/
|
|
216
308
|
declare function mapsSearchUrl(query: string, opts?: {
|
|
217
309
|
hl?: string;
|
|
218
310
|
gl?: string;
|
|
311
|
+
center?: {
|
|
312
|
+
lat: number;
|
|
313
|
+
lng: number;
|
|
314
|
+
zoom?: number;
|
|
315
|
+
};
|
|
219
316
|
}): string;
|
|
220
317
|
/**
|
|
221
318
|
* Expand a possibly-multi search request into individual `{type, city, area}`
|
|
@@ -280,10 +377,11 @@ declare function callingCode(country?: string): string | undefined;
|
|
|
280
377
|
*/
|
|
281
378
|
declare function normalizePhone(raw?: string, country?: string): string | undefined;
|
|
282
379
|
/** Keys a lead list can be sorted by. */
|
|
283
|
-
type SortKey = "rating" | "reviews" | "name" | "priceLevel";
|
|
380
|
+
type SortKey = "rating" | "reviews" | "name" | "priceLevel" | "distance";
|
|
284
381
|
/**
|
|
285
382
|
* Sort leads by a key, missing values always last. Stable, pure, new array.
|
|
286
|
-
* `dir` defaults to descending for numeric keys
|
|
383
|
+
* `dir` defaults to descending for numeric quality keys, ascending for `name`
|
|
384
|
+
* and `distance` (nearest first).
|
|
287
385
|
*/
|
|
288
386
|
declare function sortLeads(leads: Lead[], by?: SortKey, dir?: "asc" | "desc"): Lead[];
|
|
289
387
|
|
|
@@ -320,6 +418,34 @@ declare function enrichContacts(website: string, opts?: {
|
|
|
320
418
|
contactPages?: boolean;
|
|
321
419
|
}): Promise<Contacts>;
|
|
322
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Geo helpers for radius search — parse a coordinate/radius, measure distance
|
|
423
|
+
* (haversine), and pick a Maps zoom that frames a radius. All pure, unit-tested.
|
|
424
|
+
*/
|
|
425
|
+
/** A latitude/longitude point. */
|
|
426
|
+
interface LatLng {
|
|
427
|
+
lat: number;
|
|
428
|
+
lng: number;
|
|
429
|
+
}
|
|
430
|
+
/** Great-circle distance between two points, in metres. Pure. */
|
|
431
|
+
declare function haversineMeters(a: LatLng, b: LatLng): number;
|
|
432
|
+
/**
|
|
433
|
+
* Parse a `"lat,lng"` pair (also accepts a bare `@lat,lng` or whitespace).
|
|
434
|
+
* Returns `undefined` if it isn't two in-range numbers. Pure.
|
|
435
|
+
*/
|
|
436
|
+
declare function parseLatLngPair(input: string | undefined): LatLng | undefined;
|
|
437
|
+
/**
|
|
438
|
+
* Parse a distance like `"2km"`, `"500m"`, `"1.5mi"` (or a bare number = metres)
|
|
439
|
+
* into metres. Returns `undefined` for junk or non-positive values. Pure.
|
|
440
|
+
*/
|
|
441
|
+
declare function parseDistance(input: string | undefined): number | undefined;
|
|
442
|
+
/**
|
|
443
|
+
* A Google Maps zoom level (integer) that roughly frames a circle of `radiusM`
|
|
444
|
+
* at a given latitude in the map viewport. Clamped to 3–19. Pure/best-effort —
|
|
445
|
+
* Maps re-fits results anyway; this just centres the search sensibly.
|
|
446
|
+
*/
|
|
447
|
+
declare function zoomForRadius(radiusM: number, lat?: number): number;
|
|
448
|
+
|
|
323
449
|
/** An arbitrary tabular row. */
|
|
324
450
|
type DataRow = Record<string, unknown>;
|
|
325
451
|
/** Infer the format from a filename extension. Throws on an unknown extension. */
|
|
@@ -346,4 +472,4 @@ declare function convertFile(input: string, opts?: {
|
|
|
346
472
|
count: number;
|
|
347
473
|
}>;
|
|
348
474
|
|
|
349
|
-
export { ALL_FIELDS, type BatchQuery, CALLING_CODES, type Contacts, DEFAULT_FIELDS, type DataRow, ENRICHED_FIELDS, FIELD_PRESETS, type Lead, type LeadField, type LeadFilters, LeadsError, type OutputFormat, type SearchOptions, type SortKey$1 as SortKey, callingCode, cleanWebsite, columnsOf, composeQuery, convertFile, dedupeLeads, defaultFilename, detectFormat, enrichContacts, expandQueries, extractEmails, extractSocials, filterLeads, mapsSearchUrl, normalizeFields, normalizePhone, parseLatLng, parseRating, parseReviewCount, readRows, resolvePreset, scrapeLeads, scrapeLeads as searchLeads, searchLeadsBatch, searchLeadsMulti, serialize, serializeRows, sortLeads, toRows };
|
|
475
|
+
export { ALL_FIELDS, type BatchQuery, CALLING_CODES, type Contacts, DEFAULT_FIELDS, type DataRow, type DetailedResult, ENRICHED_FIELDS, type EmailStatus, FIELD_PRESETS, type LatLng, type Lead, type LeadField, type LeadFilters, type LeadStats, LeadsError, type OutputFormat, type SearchOptions, type SortKey$1 as SortKey, callingCode, cleanWebsite, columnsOf, composeQuery, computeStats, convertFile, dedupeLeads, defaultFilename, detectFormat, emailDomain, emailFormatValid, enrichContacts, expandQueries, extractEmails, extractSocials, filterLeads, haversineMeters, mapsSearchUrl, normalizeFields, normalizePhone, parseDistance, parseLatLng, parseLatLngPair, parseRating, parseReviewCount, readRows, resolvePreset, rowsToLeads, scrapeLeads, scrapeLeads as searchLeads, searchLeadsBatch, searchLeadsDetailed, searchLeadsMulti, serialize, serializeRows, sortLeads, toRows, verifyEmail, verifyEmails, zoomForRadius };
|