lacspace-leads 1.4.0 → 1.6.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
@@ -1,6 +1,6 @@
1
1
  # lacspace-leads
2
2
 
3
- **Free, open-source local-business lead finder.** Name a city, area and business type — it drives a real browser over Google Maps and collects each listing's **name, category, rating, reviews, address, phone, website, email and social links**, then exports to **JSON, NDJSON, CSV or Excel**. No API keys, no paid services.
3
+ **Free, open-source local-business lead finder.** Name a city, area and business type — it drives a real browser over Google Maps and collects each listing's **name, category & tags, rating, reviews, price level, opening hours, open-now/business status, address, phone, website, email and social links**, then exports to **JSON, NDJSON, CSV or Excel**. No API keys, no paid services.
4
4
 
5
5
  ```bash
6
6
  npx lacspace-leads restaurants --city Kathmandu --area Baneshwor -f xlsx
@@ -19,7 +19,11 @@ That opens a browser, searches Maps for *"restaurants in Baneshwor, Kathmandu"*,
19
19
  - **CRM-ready data** — normalise phones to **E.164** (`--country NP` → `+9779…`), and tidy website URLs (unwrap Google redirects, strip `utm_*`/`fbclid`).
20
20
  - **Any format** — JSON, NDJSON, CSV or Excel; write to a file or pipe to `stdout` with `-o -`.
21
21
  - **Pick your fields** — choose exactly what you collect, or a ready-made **preset** (`--preset outreach`).
22
- - **Sort & filter** — `--sort reviews --desc`, `--min-rating`, `--has-valid-email`, and more.
22
+ - **Sort & filter** — `--sort reviews --desc`, `--min-rating`, `--open-now`, `--price 2`, `--category coffee`, `--business-status operational`, `--has-valid-email`, and more.
23
+ - **Resumable sweeps** — `--resume` checkpoints a long city sweep after every search, so a crash or CAPTCHA doesn't lose the run — just re-run and it continues.
24
+ - **Run summary** — `--summary` prints rating bands, contactability (% with phone/website/email) and the top categories after a run.
25
+ - **Cross-file dedupe** — `--dedupe-across master.csv` writes only leads you don't already have.
26
+ - **Pipe to enrich** — `--enrich-out sites.ndjson` (or the `pipeToEnrich` hook) hands websites straight to [`lacspace-enrich`](https://www.npmjs.com/package/lacspace-enrich) for full company profiles — no hard dependency.
23
27
  - **Robust & polite** — `--proxy`, `--retries`, `--jitter`, per-listing delays and a permission-first prompt before it opens a browser.
24
28
  - **Library too** — `import { searchLeads, searchLeadsBatch, searchLeadsDetailed } from "lacspace-leads"`.
25
29
 
@@ -45,11 +49,15 @@ npx lacspace-leads [type] [options]
45
49
  | `-q, --query <text>` | Raw query, used verbatim (overrides city/area/type) |
46
50
  | `--near <lat,lng>` | Centre the search on a coordinate (radius search) |
47
51
  | `--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` |
52
+ | `--fields <list>` | Columns: `name,category,categories,rating,reviews,priceLevel,businessStatus,claimed,openNow,address,phone,website,email,facebook,instagram,whatsapp,linkedin,twitter,youtube,tiktok,telegram,emailStatus,plusCode,latitude,longitude,distanceKm,hours,mapsUrl` |
49
53
  | `--preset <name>` | Field bundle: `minimal` · `outreach` · `contact` · `geo` · `full` · `everything` |
50
54
  | `-f, --format <fmt>` | `json` · `ndjson` · `csv` · `xlsx` (default `json`) |
51
55
  | `-o, --out <file>` | Output file, or `-` for **stdout** (default: a slug + date) |
52
56
  | `--append` | Merge into an existing output file — **accumulate + dedupe** across runs |
57
+ | `--dedupe-across <file>` | Drop leads already present in an existing master file before writing (only new ones) |
58
+ | `--summary` | Print run stats after collecting — rating bands, % with phone/website/email, top categories |
59
+ | `--enrich-out <file>` | Also write `{ name, website, domain }` NDJSON, ready to feed [`lacspace-enrich`](https://www.npmjs.com/package/lacspace-enrich) |
60
+ | `--resume` | Resume an interrupted **multi-search sweep** from its checkpoint file (`<out>.checkpoint.json`) |
53
61
  | `--sheet <name>` | Excel sheet name (default `Leads`) |
54
62
  | `-n, --limit <n>` | Max listings **per search** (default `60`) |
55
63
  | `--total <n>` | Cap the merged result when sweeping several searches |
@@ -93,6 +101,12 @@ npx lacspace-leads [type] [options]
93
101
  | `--has-website` | Only leads with a website |
94
102
  | `--has-email` | Only leads with an email (implies `--emails`) |
95
103
  | `--has-valid-email` | Only leads whose email passed **MX verification** (implies `--verify-emails`) |
104
+ | `--has-contact` | Only leads reachable by **phone, email or website** |
105
+ | `--open-now` | Only leads **open at scrape time** (from the hours widget) |
106
+ | `--price <1-4>` | Only leads at this price tier — `$`=1 … `$$$$`=4 |
107
+ | `--category <text>` | Only leads whose **primary or tag category** contains this text (case-insensitive) |
108
+ | `--business-status <s>` | Only this status — `operational` · `closed` · `temporarily-closed` |
109
+ | `--name-exclude <list>` | Drop leads whose name contains any of these terms (comma-separated) |
96
110
  | `--dedupe <key>` | `website` · `phone` · `name` · `smart` · `none` (default `website`; `--append` uses `smart` = website→phone→name) |
97
111
 
98
112
  Run with no arguments for an interactive walkthrough.
@@ -139,6 +153,8 @@ Skip spelling out `--fields` with a ready-made bundle:
139
153
  npx lacspace-leads salons --city Pokhara --preset outreach --country NP -f csv
140
154
  ```
141
155
 
156
+ > **Honest note on the extra listing fields.** `priceLevel`, `businessStatus`, `claimed`, `openNow`, `hours` and category `categories` are read from whatever Google renders on the listing panel — they're **best-effort** and simply left blank when Maps doesn't show them (`claimed`/`businessStatus` fall back to `true`/`operational` for a normally-loaded listing). The parsing itself is pure and unit-tested, but Google's markup shifts over time; treat these as helpful hints, not guarantees. `--open-now` reflects the state *at scrape time*.
157
+
142
158
  ## Verify emails & build a master list
143
159
 
144
160
  Chase down deliverable contacts and accumulate them over time:
@@ -154,6 +170,102 @@ npx lacspace-leads cafes --city Kathmandu --area "Thamel,Baneshwor,Patan" \
154
170
 
155
171
  `--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
172
 
173
+ ## Saved campaigns (`--config`)
174
+
175
+ Describe a repeatable set of searches plus shared options in one JSON file, and run them all with a single command — ideal for the same city sweeps every week (pair it with cron or CI):
176
+
177
+ ```jsonc
178
+ // campaign.json
179
+ {
180
+ "searches": [
181
+ { "type": "cafes", "city": "Kathmandu", "area": "Thamel,Baneshwor,Patan" },
182
+ { "type": "gyms", "city": "Pokhara" }
183
+ ],
184
+ "country": "NP",
185
+ "verifyEmails": true,
186
+ "sort": "reviews",
187
+ "filters": { "hasContact": true, "excludeNames": ["closed"] },
188
+ "out": "master.xlsx",
189
+ "format": "xlsx",
190
+ "append": true
191
+ }
192
+ ```
193
+
194
+ ```bash
195
+ npx lacspace-leads --config campaign.json
196
+ ```
197
+
198
+ Every search is run, merged and de-duplicated; the shared options, filters and output settings apply across the whole campaign. Programmatically: `runConfig(config)`.
199
+
200
+ ## Resume a long sweep (`--resume`)
201
+
202
+ Sweeping a whole city one neighbourhood at a time can take a while, and Google may throw a CAPTCHA halfway through. Add `--resume` and every completed sub-search is checkpointed to `<out>.checkpoint.json` — the leads gathered so far plus which searches are done. Re-run the exact same command and it **skips the areas already collected** and picks up where it stopped; when the sweep finishes cleanly the checkpoint is deleted.
203
+
204
+ ```bash
205
+ npx lacspace-leads cafes --city Kathmandu \
206
+ --area "Thamel,Baneshwor,Patan,Jhamsikhel,Boudha,Kirtipur" \
207
+ --resume -o sweep.csv
208
+ # crashes after Patan? just run it again — Thamel/Baneshwor/Patan are skipped.
209
+ ```
210
+
211
+ `--resume` applies to multi-search sweeps and `--config` campaigns (it's a no-op for a single search). The checkpoint key normalises case/whitespace, so the same area sweep always lines up.
212
+
213
+ ## Run summary (`--summary`)
214
+
215
+ Add `--summary` for a quick read on what you got — coverage, rating bands and the top categories — printed after the run:
216
+
217
+ ```bash
218
+ npx lacspace-leads bars --city Pokhara --open-now --price 2 --summary
219
+ ```
220
+
221
+ ```text
222
+ Summary — 43 leads
223
+ contactable: 88% phone · 51% website · 23% email (12 with a social link)
224
+ avg rating: 4.3 ★
225
+ rating bands: 4.5+ ×14 · 4.0–4.4 ×19 · 3.0–3.9 ×8 · unrated ×2
226
+ open now: 43
227
+ top categories: Bar (18), Restaurant (11), Pub (7), Night club (4)
228
+ ```
229
+
230
+ In code, `summarize(leads)` returns the structured stats and `formatSummary(summary)` renders the text.
231
+
232
+ ## Only what's new (`--dedupe-across`)
233
+
234
+ Already have a master list and only want the businesses you *don't* have yet? Point `--dedupe-across` at it and any lead whose identity (by `--dedupe` key, default `smart`) is already in that file is dropped **before** writing:
235
+
236
+ ```bash
237
+ # Collect gyms, but write only the ones missing from master.csv, then append them
238
+ npx lacspace-leads gyms --city Lalitpur --dedupe-across master.csv -o new.csv --append
239
+ ```
240
+
241
+ This differs from `--append` (which merges everything into one file): `--dedupe-across` filters *this run* against a **separate** reference file, so `new.csv` holds only genuinely new prospects. In code: `subtractLeads(leads, master, "smart")`.
242
+
243
+ ## Pipe into `lacspace-enrich`
244
+
245
+ lacspace-leads finds businesses and their websites; its sibling [`lacspace-enrich`](https://www.npmjs.com/package/lacspace-enrich) turns a domain into a full company + contact + tech-stack profile. Bridge the two **without a hard dependency**:
246
+
247
+ ```bash
248
+ # Write one { name, website, domain } object per line, ready for enrich
249
+ npx lacspace-leads clinics --city Pokhara --enrich-out sites.ndjson -o clinics.csv
250
+ ```
251
+
252
+ Or stream websites into your own enrich pipeline as leads are found, using the `onLead`-compatible `pipeToEnrich` factory:
253
+
254
+ ```ts
255
+ import { searchLeads, pipeToEnrich, leadsToEnrichInput } from "lacspace-leads";
256
+ // import { enrich } from "lacspace-enrich"; // install separately, optional
257
+
258
+ const queue: { website: string; domain?: string }[] = [];
259
+ const leads = await searchLeads({
260
+ type: "cafes", city: "Kathmandu",
261
+ onLead: pipeToEnrich((input) => queue.push(input)), // { name, website, domain }
262
+ });
263
+
264
+ // or after the fact — one unique input per domain:
265
+ const inputs = leadsToEnrichInput(leads);
266
+ // for (const { domain } of inputs) await enrich(domain);
267
+ ```
268
+
157
269
  ## Convert your leads to any format
158
270
 
159
271
  Collected leads as JSON and now need them in Excel? A general converter is built in — it reads and writes **JSON · NDJSON · CSV · Excel** in any direction, and works on *any* tabular file, not just leads:
@@ -289,7 +401,11 @@ const leads = await searchLeadsBatch(
289
401
  | `enrichContacts(website)` / `extractEmails` / `extractSocials` | Website enrichment, on tap. |
290
402
  | `cleanWebsite` / `normalizePhone` / `sortLeads` | Pure data-cleaning helpers (unit-tested). |
291
403
  | `haversineMeters` / `parseLatLngPair` / `parseDistance` | Pure geo helpers for radius search. |
292
- | `filterLeads` / `dedupeLeads` | Pure post-processing over any `Lead[]`. |
404
+ | `filterLeads` / `dedupeLeads` / `subtractLeads` | Pure post-processing — filter, in-list dedupe, and cross-file dedupe. |
405
+ | `summarize(leads)` / `formatSummary(s)` | Rating bands, contact coverage %, top categories — the `--summary` engine. |
406
+ | `parsePriceLevel` / `priceLevelValue` / `parseBusinessStatus` / `parseClaimed` / `parseOpenNow` / `parseCategoryTags` | Pure field parsers (unit-tested against HTML/aria snippets). |
407
+ | `pipeToEnrich(handler)` / `leadsToEnrichInput` / `leadDomains` / `leadDomain` | Bridge collected leads to `lacspace-enrich` (no hard dep). |
408
+ | `queryKey` / `pendingQueries` / `recordQuery` / `loadCheckpoint` / `saveCheckpoint` / `clearCheckpoint` | Resume/checkpoint primitives; pass `skip`/`seedLeads`/`onQueryDone` to `searchLeadsBatch`. |
293
409
  | `expandQueries` / `resolvePreset` / `FIELD_PRESETS` | Batch expansion + field presets. |
294
410
  | `composeQuery` / `mapsSearchUrl` / `normalizeFields` / `defaultFilename` | Query + helper utilities. |
295
411