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 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** and more, then exports to **JSON, 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, rating, reviews, 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
@@ -11,10 +11,16 @@ That opens a browser, searches Maps for *"restaurants in Baneshwor, Kathmandu"*,
11
11
  ## Why it's different
12
12
 
13
13
  - **Free & keyless** — uses a real browser (via [Playwright](https://playwright.dev)), not a paid Places API.
14
- - **Any format** — JSON, CSV or Excel out of the box (Excel/CSV via `@lacspace/xlsx` + `@lacspace/csv`).
15
- - **Pick your fields** — only collect what you need.
16
- - **Permission-first** — it tells you what it's about to do and asks before opening a browser.
17
- - **Library too** — `import { searchLeads } from "lacspace-leads"`.
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
+ - **Accumulate a master list** — `--append` merges each run into your existing file and de-duplicates, so daily runs build one clean database.
16
+ - **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.
17
+ - **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.
18
+ - **CRM-ready data** — normalise phones to **E.164** (`--country NP` → `+9779…`), and tidy website URLs (unwrap Google redirects, strip `utm_*`/`fbclid`).
19
+ - **Any format** — JSON, NDJSON, CSV or Excel; write to a file or pipe to `stdout` with `-o -`.
20
+ - **Pick your fields** — choose exactly what you collect, or a ready-made **preset** (`--preset outreach`).
21
+ - **Sort & filter** — `--sort reviews --desc`, `--min-rating`, `--has-valid-email`, and more.
22
+ - **Robust & polite** — `--proxy`, `--retries`, `--jitter`, per-listing delays and a permission-first prompt before it opens a browser.
23
+ - **Library too** — `import { searchLeads, searchLeadsBatch, searchLeadsDetailed } from "lacspace-leads"`.
18
24
 
19
25
  ## Install
20
26
 
@@ -32,18 +38,28 @@ npx lacspace-leads [type] [options]
32
38
 
33
39
  | Option | Meaning |
34
40
  | --- | --- |
35
- | `-t, --type <text>` | Business type / keyword, e.g. `"dental clinic"` |
36
- | `--city <text>` | City, e.g. `Kathmandu` |
37
- | `--area <text>` | Area / neighbourhood, e.g. `Baneshwor` |
41
+ | `-t, --type <text>` | Business type / keyword, e.g. `"dental clinic"`. Comma-separate for several. |
42
+ | `--city <text>` | City, e.g. `Kathmandu`. Comma-separate for several. |
43
+ | `--area <text>` | Area / neighbourhood, e.g. `Baneshwor`. Comma-separate to **sweep a whole city**. |
38
44
  | `-q, --query <text>` | Raw query, used verbatim (overrides city/area/type) |
39
- | `--fields <list>` | Columns: `name,category,rating,reviews,priceLevel,address,phone,website,email,facebook,instagram,whatsapp,plusCode,latitude,longitude,hours,mapsUrl` |
40
- | `-f, --format <fmt>` | `json` · `csv` · `xlsx` (default `json`) |
41
- | `-o, --out <file>` | Output file (default: a slug + date) |
45
+ | `--fields <list>` | Columns: `name,category,rating,reviews,priceLevel,address,phone,website,email,facebook,instagram,whatsapp,linkedin,twitter,youtube,tiktok,telegram,emailStatus,plusCode,latitude,longitude,hours,mapsUrl` |
46
+ | `--preset <name>` | Field bundle: `minimal` · `outreach` · `contact` · `geo` · `full` · `everything` |
47
+ | `-f, --format <fmt>` | `json` · `ndjson` · `csv` · `xlsx` (default `json`) |
48
+ | `-o, --out <file>` | Output file, or `-` for **stdout** (default: a slug + date) |
49
+ | `--append` | Merge into an existing output file — **accumulate + dedupe** across runs |
42
50
  | `--sheet <name>` | Excel sheet name (default `Leads`) |
43
- | `-n, --limit <n>` | Max listings to collect (default `60`) |
51
+ | `-n, --limit <n>` | Max listings **per search** (default `60`) |
52
+ | `--total <n>` | Cap the merged result when sweeping several searches |
44
53
  | `--no-details` | Skip opening each listing — names + Maps URLs only, much faster |
54
+ | `--sort <key>` | `rating` · `reviews` · `name` · `priceLevel` (missing values last) |
55
+ | `--desc` / `--asc` | Sort direction (default: `desc` for numbers, `asc` for name) |
45
56
  | `--delay <ms>` | Pause between listings (default `700`) |
57
+ | `--jitter` | Randomise the delay ±40% (more human) |
58
+ | `--retries <n>` | Retry a listing that fails to open (default `1`) |
46
59
  | `--max-time <s>` | Stop collecting after n seconds |
60
+ | `--proxy <url>` | Route the browser via a proxy (`http://user:pass@host:port`) |
61
+ | `--lang <locale>` | Browser locale, e.g. `en-US`, `ne-NP` (default `en-US`) |
62
+ | `--region <cc>` | Region bias for results, e.g. `np`, `us` |
47
63
  | `--headless` | Run the browser without a visible window |
48
64
  | `-y, --yes` | Skip prompts and the browser-open confirmation |
49
65
 
@@ -52,8 +68,17 @@ npx lacspace-leads [type] [options]
52
68
  | Option | Meaning |
53
69
  | --- | --- |
54
70
  | `--emails` | Also find an email from each website |
55
- | `--socials` | Also find Facebook / Instagram / WhatsApp |
71
+ | `--socials` | Also find Facebook / Instagram / WhatsApp / LinkedIn / X / YouTube / TikTok / Telegram |
56
72
  | `--enrich` | Both of the above |
73
+ | `--verify-emails` | Check each email domain has **MX records** (implies `--emails`); adds an `emailStatus` column |
74
+ | `--concurrency <n>` | How many websites to enrich in parallel (default `3`) |
75
+
76
+ **Clean-up** (data quality):
77
+
78
+ | Option | Meaning |
79
+ | --- | --- |
80
+ | `--country <c>` | Normalise phones to **E.164** for this country — an ISO-2 code (`NP`, `US`) or a calling code (`977`) |
81
+ | `--no-clean-urls` | Don't tidy website URLs (by default it unwraps Google redirects and strips tracking params) |
57
82
 
58
83
  **Filters** (drop leads that don't qualify):
59
84
 
@@ -64,10 +89,57 @@ npx lacspace-leads [type] [options]
64
89
  | `--has-phone` | Only leads with a phone |
65
90
  | `--has-website` | Only leads with a website |
66
91
  | `--has-email` | Only leads with an email (implies `--emails`) |
67
- | `--dedupe <key>` | `website` · `phone` · `name` · `none` (default `website`) |
92
+ | `--has-valid-email` | Only leads whose email passed **MX verification** (implies `--verify-emails`) |
93
+ | `--dedupe <key>` | `website` · `phone` · `name` · `smart` · `none` (default `website`; `--append` uses `smart` = website→phone→name) |
68
94
 
69
95
  Run with no arguments for an interactive walkthrough.
70
96
 
97
+ ## Sweep a whole city
98
+
99
+ Comma-separate areas (and/or types) and `lacspace-leads` runs each search in turn, then **merges and de-duplicates** into a single list — sorted and filtered across the whole set:
100
+
101
+ ```bash
102
+ # Every coffee shop across three neighbourhoods, phones as +977…, best-reviewed first
103
+ npx lacspace-leads "coffee shop" \
104
+ --city Kathmandu --area "Thamel,Baneshwor,Patan" \
105
+ --country NP --sort reviews --desc -f xlsx
106
+
107
+ # Two business types at once, capped at 100 unique leads total
108
+ npx lacspace-leads --type "gym,fitness studio" --city Pokhara --total 100 -f csv
109
+ ```
110
+
111
+ ## Field presets
112
+
113
+ Skip spelling out `--fields` with a ready-made bundle:
114
+
115
+ | Preset | Columns |
116
+ | --- | --- |
117
+ | `minimal` | name, phone, website |
118
+ | `outreach` | name, phone, email, website, address |
119
+ | `contact` | name, phone, email, website, facebook, instagram, whatsapp |
120
+ | `geo` | name, address, latitude, longitude, plusCode, mapsUrl |
121
+ | `full` | everything Maps shows (no website enrichment) |
122
+ | `everything` | every field, including enriched ones |
123
+
124
+ ```bash
125
+ npx lacspace-leads salons --city Pokhara --preset outreach --country NP -f csv
126
+ ```
127
+
128
+ ## Verify emails & build a master list
129
+
130
+ Chase down deliverable contacts and accumulate them over time:
131
+
132
+ ```bash
133
+ # Only businesses with an MX-verified email, as CSV
134
+ npx lacspace-leads dentists --city Pokhara --verify-emails --has-valid-email -f csv
135
+
136
+ # Run this daily — each run merges into master.csv and de-duplicates (smart key)
137
+ npx lacspace-leads cafes --city Kathmandu --area "Thamel,Baneshwor,Patan" \
138
+ -o master.csv --append
139
+ ```
140
+
141
+ `--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.
142
+
71
143
  ## Convert anything (JSON ↔ CSV ↔ Excel)
72
144
 
73
145
  A general converter is built in — it works on any tabular file, not just leads:
@@ -113,13 +185,40 @@ const { data, binary } = serialize(leads, "xlsx");
113
185
  writeFileSync("leads.xlsx", binary ? Buffer.from(data as Uint8Array) : data);
114
186
  ```
115
187
 
188
+ Sweep several areas and merge them yourself:
189
+
190
+ ```ts
191
+ import { searchLeadsBatch } from "lacspace-leads";
192
+
193
+ const leads = await searchLeadsBatch(
194
+ [
195
+ { type: "coffee shop", city: "Kathmandu", area: "Thamel" },
196
+ { type: "coffee shop", city: "Kathmandu", area: "Baneshwor" },
197
+ ],
198
+ { limit: 40, country: "NP", sort: "reviews", enrich: true, headless: true },
199
+ );
200
+ // → one de-duplicated, sorted, E.164-normalised list
201
+ ```
202
+
116
203
  | Export | Purpose |
117
204
  | --- | --- |
118
- | `searchLeads(options)` | Run the search, resolve to `Lead[]`. |
119
- | `serialize(leads, format, fields?)` | Serialize to `{ data, binary }` for `json` / `csv` / `xlsx`. |
205
+ | `searchLeads(options)` | Run one search, resolve to `Lead[]`. |
206
+ | `searchLeadsDetailed(options)` | Same, plus aggregate `stats` and `elapsedMs`. |
207
+ | `searchLeadsBatch(queries, options)` | Run several searches and merge/dedupe/sort into one `Lead[]`. |
208
+ | `searchLeadsMulti(options)` | Expand comma-separated `type`/`city`/`area` and run as a batch. |
209
+ | `serialize(leads, format, fields?)` | Serialize to `{ data, binary }` for `json` / `ndjson` / `csv` / `xlsx`. |
210
+ | `computeStats(leads)` | Aggregate counts (`withPhone`, `withValidEmail`, `avgRating` …). |
211
+ | `verifyEmails(leads)` / `verifyEmail(email)` | MX-verify emails; also `emailFormatValid` / `emailDomain`. |
212
+ | `rowsToLeads(rows)` | Turn read-back rows (CSV/Excel/JSON) into `Lead[]` — powers `--append`. |
120
213
  | `toRows(leads, fields?)` | Header-keyed rows, for your own exporter. |
214
+ | `enrichContacts(website)` / `extractEmails` / `extractSocials` | Website enrichment, on tap. |
215
+ | `cleanWebsite` / `normalizePhone` / `sortLeads` | Pure data-cleaning helpers (unit-tested). |
216
+ | `filterLeads` / `dedupeLeads` | Pure post-processing over any `Lead[]`. |
217
+ | `expandQueries` / `resolvePreset` / `FIELD_PRESETS` | Batch expansion + field presets. |
121
218
  | `composeQuery` / `mapsSearchUrl` / `normalizeFields` / `defaultFilename` | Query + helper utilities. |
122
219
 
220
+ 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**.
221
+
123
222
  ## Please use it responsibly
124
223
 
125
224
  Automated scraping of Google Maps is **against Google's Terms of Service**, and heavy use can trigger CAPTCHAs or temporary blocks. This tool is meant for **small, human-scale** collection of **public business information**. You are responsible for how you use it: keep volumes modest, add delays, respect local data-protection law (and don't collect or contact people in ways that break it), and don't resell scraped data as your own. If you need high volume or guaranteed reliability, use the official Google Places API.