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/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
- /** Drop duplicate leads by this key. Default "website" when present else "name". */
107
- dedupe?: "website" | "phone" | "name" | "none";
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 and ascending for `name`.
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
- /** Drop duplicate leads by this key. Default "website" when present else "name". */
107
- dedupe?: "website" | "phone" | "name" | "none";
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 and ascending for `name`.
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 };