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 CHANGED
@@ -12,13 +12,16 @@ That opens a browser, searches Maps for *"restaurants in Baneshwor, Kathmandu"*,
12
12
 
13
13
  - **Free & keyless** — uses a real browser (via [Playwright](https://playwright.dev)), not a paid Places API.
14
14
  - **Sweep a whole city** — comma-separate areas and it runs each search, then **merges and de-duplicates** into one list: `--area "Thamel,Baneshwor,Patan"`.
15
+ - **Search by radius** — centre on a coordinate and keep only what's within range: `--near "27.72,85.32" --radius 2km`. Each lead gets a `distanceKm`, sorted nearest-first.
16
+ - **Accumulate a master list** — `--append` merges each run into your existing file and de-duplicates, so daily runs build one clean database.
15
17
  - **Rich enrichment** — visit each website to pull an **email** and links for **Facebook, Instagram, WhatsApp, LinkedIn, X, YouTube, TikTok and Telegram** — a few sites in parallel.
18
+ - **Verified emails** — `--verify-emails` checks each address's domain has **MX records** (no message sent) and tags it `valid`/`no-mx`; `--has-valid-email` keeps only deliverable ones.
16
19
  - **CRM-ready data** — normalise phones to **E.164** (`--country NP` → `+9779…`), and tidy website URLs (unwrap Google redirects, strip `utm_*`/`fbclid`).
17
20
  - **Any format** — JSON, NDJSON, CSV or Excel; write to a file or pipe to `stdout` with `-o -`.
18
21
  - **Pick your fields** — choose exactly what you collect, or a ready-made **preset** (`--preset outreach`).
19
- - **Sort & filter** — `--sort reviews --desc`, `--min-rating`, `--has-email`, and more.
20
- - **Permission-first** — it tells you what it's about to do and asks before opening a browser.
21
- - **Library too** — `import { searchLeads, searchLeadsBatch } from "lacspace-leads"`.
22
+ - **Sort & filter** — `--sort reviews --desc`, `--min-rating`, `--has-valid-email`, and more.
23
+ - **Robust & polite** — `--proxy`, `--retries`, `--jitter`, per-listing delays and a permission-first prompt before it opens a browser.
24
+ - **Library too** — `import { searchLeads, searchLeadsBatch, searchLeadsDetailed } from "lacspace-leads"`.
22
25
 
23
26
  ## Install
24
27
 
@@ -40,18 +43,24 @@ npx lacspace-leads [type] [options]
40
43
  | `--city <text>` | City, e.g. `Kathmandu`. Comma-separate for several. |
41
44
  | `--area <text>` | Area / neighbourhood, e.g. `Baneshwor`. Comma-separate to **sweep a whole city**. |
42
45
  | `-q, --query <text>` | Raw query, used verbatim (overrides city/area/type) |
43
- | `--fields <list>` | Columns: `name,category,rating,reviews,priceLevel,address,phone,website,email,facebook,instagram,whatsapp,linkedin,twitter,youtube,tiktok,telegram,plusCode,latitude,longitude,hours,mapsUrl` |
46
+ | `--near <lat,lng>` | Centre the search on a coordinate (radius search) |
47
+ | `--radius <dist>` | Keep only leads within this of `--near`, e.g. `2km`, `500m`, `1mi` |
48
+ | `--fields <list>` | Columns: `name,category,rating,reviews,priceLevel,address,phone,website,email,facebook,instagram,whatsapp,linkedin,twitter,youtube,tiktok,telegram,emailStatus,plusCode,latitude,longitude,distanceKm,hours,mapsUrl` |
44
49
  | `--preset <name>` | Field bundle: `minimal` · `outreach` · `contact` · `geo` · `full` · `everything` |
45
50
  | `-f, --format <fmt>` | `json` · `ndjson` · `csv` · `xlsx` (default `json`) |
46
51
  | `-o, --out <file>` | Output file, or `-` for **stdout** (default: a slug + date) |
52
+ | `--append` | Merge into an existing output file — **accumulate + dedupe** across runs |
47
53
  | `--sheet <name>` | Excel sheet name (default `Leads`) |
48
54
  | `-n, --limit <n>` | Max listings **per search** (default `60`) |
49
55
  | `--total <n>` | Cap the merged result when sweeping several searches |
50
56
  | `--no-details` | Skip opening each listing — names + Maps URLs only, much faster |
51
- | `--sort <key>` | `rating` · `reviews` · `name` · `priceLevel` (missing values last) |
52
- | `--desc` / `--asc` | Sort direction (default: `desc` for numbers, `asc` for name) |
57
+ | `--sort <key>` | `rating` · `reviews` · `name` · `priceLevel` · `distance` (missing values last) |
58
+ | `--desc` / `--asc` | Sort direction (default: `desc` for quality keys, `asc` for name/distance) |
53
59
  | `--delay <ms>` | Pause between listings (default `700`) |
60
+ | `--jitter` | Randomise the delay ±40% (more human) |
61
+ | `--retries <n>` | Retry a listing that fails to open (default `1`) |
54
62
  | `--max-time <s>` | Stop collecting after n seconds |
63
+ | `--proxy <url>` | Route the browser via a proxy (`http://user:pass@host:port`) |
55
64
  | `--lang <locale>` | Browser locale, e.g. `en-US`, `ne-NP` (default `en-US`) |
56
65
  | `--region <cc>` | Region bias for results, e.g. `np`, `us` |
57
66
  | `--headless` | Run the browser without a visible window |
@@ -64,6 +73,7 @@ npx lacspace-leads [type] [options]
64
73
  | `--emails` | Also find an email from each website |
65
74
  | `--socials` | Also find Facebook / Instagram / WhatsApp / LinkedIn / X / YouTube / TikTok / Telegram |
66
75
  | `--enrich` | Both of the above |
76
+ | `--verify-emails` | Check each email domain has **MX records** (implies `--emails`); adds an `emailStatus` column |
67
77
  | `--concurrency <n>` | How many websites to enrich in parallel (default `3`) |
68
78
 
69
79
  **Clean-up** (data quality):
@@ -82,7 +92,8 @@ npx lacspace-leads [type] [options]
82
92
  | `--has-phone` | Only leads with a phone |
83
93
  | `--has-website` | Only leads with a website |
84
94
  | `--has-email` | Only leads with an email (implies `--emails`) |
85
- | `--dedupe <key>` | `website` · `phone` · `name` · `none` (default `website`) |
95
+ | `--has-valid-email` | Only leads whose email passed **MX verification** (implies `--verify-emails`) |
96
+ | `--dedupe <key>` | `website` · `phone` · `name` · `smart` · `none` (default `website`; `--append` uses `smart` = website→phone→name) |
86
97
 
87
98
  Run with no arguments for an interactive walkthrough.
88
99
 
@@ -100,6 +111,17 @@ npx lacspace-leads "coffee shop" \
100
111
  npx lacspace-leads --type "gym,fitness studio" --city Pokhara --total 100 -f csv
101
112
  ```
102
113
 
114
+ ## Search by radius
115
+
116
+ Centre the search on a coordinate and keep only what's within range — precise catchment-area targeting for delivery zones, field sales or store-radius research:
117
+
118
+ ```bash
119
+ # Restaurants within 2 km of a point, nearest first, with a distance column
120
+ npx lacspace-leads restaurants --near "27.7172,85.3240" --radius 2km -f csv
121
+ ```
122
+
123
+ Every kept lead gains a `distanceKm` column and results are sorted **nearest-first** by default (override with `--sort`). `--radius` accepts `km`, `m` or `mi` (a bare number is metres); omit it to just centre the search without a hard cutoff. Under the hood it points Google Maps at the coordinate, then filters by real great-circle (haversine) distance.
124
+
103
125
  ## Field presets
104
126
 
105
127
  Skip spelling out `--fields` with a ready-made bundle:
@@ -117,6 +139,21 @@ Skip spelling out `--fields` with a ready-made bundle:
117
139
  npx lacspace-leads salons --city Pokhara --preset outreach --country NP -f csv
118
140
  ```
119
141
 
142
+ ## Verify emails & build a master list
143
+
144
+ Chase down deliverable contacts and accumulate them over time:
145
+
146
+ ```bash
147
+ # Only businesses with an MX-verified email, as CSV
148
+ npx lacspace-leads dentists --city Pokhara --verify-emails --has-valid-email -f csv
149
+
150
+ # Run this daily — each run merges into master.csv and de-duplicates (smart key)
151
+ npx lacspace-leads cafes --city Kathmandu --area "Thamel,Baneshwor,Patan" \
152
+ -o master.csv --append
153
+ ```
154
+
155
+ `--verify-emails` does a DNS **MX lookup** on each email's domain (no message is sent) and adds an `emailStatus` column of `valid` · `no-mx` · `invalid-format`. `--append` reads the existing file back, merges, and de-duplicates with the **`smart`** key (website → phone → name) so even website-less businesses don't pile up on re-runs.
156
+
120
157
  ## Convert anything (JSON ↔ CSV ↔ Excel)
121
158
 
122
159
  A general converter is built in — it works on any tabular file, not just leads:
@@ -180,17 +217,22 @@ const leads = await searchLeadsBatch(
180
217
  | Export | Purpose |
181
218
  | --- | --- |
182
219
  | `searchLeads(options)` | Run one search, resolve to `Lead[]`. |
220
+ | `searchLeadsDetailed(options)` | Same, plus aggregate `stats` and `elapsedMs`. |
183
221
  | `searchLeadsBatch(queries, options)` | Run several searches and merge/dedupe/sort into one `Lead[]`. |
184
222
  | `searchLeadsMulti(options)` | Expand comma-separated `type`/`city`/`area` and run as a batch. |
185
223
  | `serialize(leads, format, fields?)` | Serialize to `{ data, binary }` for `json` / `ndjson` / `csv` / `xlsx`. |
224
+ | `computeStats(leads)` | Aggregate counts (`withPhone`, `withValidEmail`, `avgRating` …). |
225
+ | `verifyEmails(leads)` / `verifyEmail(email)` | MX-verify emails; also `emailFormatValid` / `emailDomain`. |
226
+ | `rowsToLeads(rows)` | Turn read-back rows (CSV/Excel/JSON) into `Lead[]` — powers `--append`. |
186
227
  | `toRows(leads, fields?)` | Header-keyed rows, for your own exporter. |
187
228
  | `enrichContacts(website)` / `extractEmails` / `extractSocials` | Website enrichment, on tap. |
188
229
  | `cleanWebsite` / `normalizePhone` / `sortLeads` | Pure data-cleaning helpers (unit-tested). |
230
+ | `haversineMeters` / `parseLatLngPair` / `parseDistance` | Pure geo helpers for radius search. |
189
231
  | `filterLeads` / `dedupeLeads` | Pure post-processing over any `Lead[]`. |
190
232
  | `expandQueries` / `resolvePreset` / `FIELD_PRESETS` | Batch expansion + field presets. |
191
233
  | `composeQuery` / `mapsSearchUrl` / `normalizeFields` / `defaultFilename` | Query + helper utilities. |
192
234
 
193
- Everything is fully typed (`Lead`, `LeadField`, `SearchOptions`, `SortKey`, `OutputFormat`, `Contacts` …) and ships dual **ESM + CJS**.
235
+ The `onProgress` and `onLead` callbacks stream progress and each lead as it's found — handy for live UIs or crash-safe writing. Everything is fully typed (`Lead`, `LeadField`, `SearchOptions`, `LeadStats`, `EmailStatus`, `SortKey`, `OutputFormat`, `Contacts` …) and ships dual **ESM + CJS**.
194
236
 
195
237
  ## Please use it responsibly
196
238