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 +115 -16
- package/dist/cli.js +680 -126
- package/dist/lib.cjs +577 -99
- package/dist/lib.d.cts +221 -8
- package/dist/lib.d.ts +221 -8
- package/dist/lib.js +554 -94
- package/package.json +7 -2
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
|
|
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
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
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
|
-
|
|
|
41
|
-
| `-
|
|
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
|
|
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
|
-
| `--
|
|
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
|
|
119
|
-
| `
|
|
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.
|