lacspace-leads 1.7.1 → 1.8.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/LICENSE +51 -0
- package/README.md +38 -3
- package/dist/cli.js +445 -136
- package/dist/lib.cjs +317 -101
- package/dist/lib.d.cts +53 -2
- package/dist/lib.d.ts +53 -2
- package/dist/lib.js +314 -100
- package/package.json +4 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Lacspace Free Licence
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Lacspace
|
|
5
|
+
|
|
6
|
+
PREAMBLE
|
|
7
|
+
|
|
8
|
+
This software is published by Lacspace under the Lacspace Free Licence — a free,
|
|
9
|
+
permissive licence that lets you use this software for any purpose, including in
|
|
10
|
+
commercial products and services, at no cost. It grants the same freedoms as
|
|
11
|
+
common permissive open-source licences; the only condition is that this notice
|
|
12
|
+
travels with the software. The canonical, always-current text of this licence is
|
|
13
|
+
maintained at https://lacspace.com/licenses/lacspace-free-1.0
|
|
14
|
+
|
|
15
|
+
GRANT OF RIGHTS
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person or organisation
|
|
18
|
+
obtaining a copy of this software and its associated documentation and data files
|
|
19
|
+
(the "Software"), to deal in the Software without restriction, including without
|
|
20
|
+
limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
21
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
22
|
+
Software is furnished to do so, subject to the conditions below. These rights are
|
|
23
|
+
granted for any purpose, personal or commercial, and are perpetual, worldwide,
|
|
24
|
+
non-exclusive, and royalty-free.
|
|
25
|
+
|
|
26
|
+
CONDITIONS
|
|
27
|
+
|
|
28
|
+
The above copyright notice, this permission notice, and the name of this licence
|
|
29
|
+
("Lacspace Free Licence") shall be included in all copies or substantial portions
|
|
30
|
+
of the Software.
|
|
31
|
+
|
|
32
|
+
TRADEMARKS
|
|
33
|
+
|
|
34
|
+
This licence does not grant permission to use the trade names, trademarks, service
|
|
35
|
+
marks, logos, or product names of Lacspace, except as required to reproduce the
|
|
36
|
+
notice above or to describe the origin of the Software in a truthful manner.
|
|
37
|
+
|
|
38
|
+
DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
42
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
43
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
|
|
44
|
+
AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
|
|
45
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
The Lacspace Free Licence is a source-available, permissive licence and is not (as
|
|
50
|
+
of this version) an OSI-approved licence. In substance it grants the same freedoms
|
|
51
|
+
as the MIT Licence. Learn more at https://lacspace.com/licenses
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ That opens a browser, searches Maps for *"restaurants in Baneshwor, Kathmandu"*,
|
|
|
10
10
|
|
|
11
11
|
## Why it's different
|
|
12
12
|
|
|
13
|
+
- **Live file, row by row** — every lead is written to your file the moment it's found: CSV/NDJSON rows are appended, JSON stays a valid array, Excel is rewritten each time. Open it mid-run and the rows are there; stop with Ctrl-C and nothing collected is lost. `--resume` carries on from the file.
|
|
13
14
|
- **Free & keyless** — uses a real browser (via [Playwright](https://playwright.dev)), not a paid Places API.
|
|
14
15
|
- **Ask for a number, get that number** — one Google search stops at ~120 results; `--target 500` keeps searching (every area you name, then tiles of the map) until it has 500 unique businesses.
|
|
15
16
|
- **Sweep a whole city** — comma-separate areas and it runs each search, then **merges and de-duplicates** into one list: `--area "Thamel,Baneshwor,Patan"`.
|
|
@@ -151,6 +152,8 @@ npx lacspace-leads --config campaign.json
|
|
|
151
152
|
| Far fewer leads than the target | The map ran out — that really is everything Google lists for that query | More areas or cities, related `--type`s, or a bigger `--step` |
|
|
152
153
|
| About 114 back from `--limit 500` | Google's per-search ceiling | Use `--target 500` instead |
|
|
153
154
|
| No leads at all | Usually a CAPTCHA or a page change | Drop `--headless` to watch it, raise `--delay`, try again shortly |
|
|
155
|
+
| Empty "Reviews" column | Google currently hides review counts from signed-out visitors (the rating still shows) | Nothing to fix on our side; sort by `rating` instead |
|
|
156
|
+
| Excel file "can't be written" warning | The file is open in Excel, which locks it | Rows are kept and written as soon as you close it |
|
|
154
157
|
| Empty address / website columns | `--no-details` was on, so listings were never opened | Remove it (`--no-website` and `--split` turn details back on for you) |
|
|
155
158
|
| Everything grouped under "other" with `--split` | Same cause — no addresses to match against | Same fix |
|
|
156
159
|
|
|
@@ -361,6 +364,23 @@ npx lacspace-leads --config campaign.json
|
|
|
361
364
|
|
|
362
365
|
Every search is run, merged and de-duplicated; the shared options, filters and output settings apply across the whole campaign. Programmatically: `runConfig(config)`.
|
|
363
366
|
|
|
367
|
+
## Live output: watch the file fill up
|
|
368
|
+
|
|
369
|
+
Ask for 100 leads and the file gets row 1 as soon as the first business is read, row 2 a few seconds later, and so on. Each lead is finished before it is written: website cleaned, email and socials found (in the background, while the browser opens the next listing), email verified, phone normalised, de-duplicated and checked against every filter. Each row is a final row, and a `--target 100` with filters stops at 100 *matching* leads.
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
✚ 37/100 Himalayan Java Coffee — 01-4422519 · himalayanjava.com (14.2/min, ~5 min left)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
| Format | While it runs |
|
|
376
|
+
| --- | --- |
|
|
377
|
+
| `csv` | header first, then one appended line per lead |
|
|
378
|
+
| `ndjson` | one appended line per lead (also streams live to stdout with `-o -`) |
|
|
379
|
+
| `json` | always a valid JSON array: the closing `]` is moved after each lead |
|
|
380
|
+
| `xlsx` | the workbook is rewritten safely (temp file + rename) after each lead |
|
|
381
|
+
|
|
382
|
+
When the run ends the file is rewritten once more in its final order (`--sort`, `--append` merges). Press **Ctrl-C** and it stops cleanly, saving everything collected. Then run the same command with `--resume`: it keeps the rows in the file, skips those listings and fills up to the `--target`. `--no-live` turns this off and writes only at the end.
|
|
383
|
+
|
|
364
384
|
## Resume a long sweep (`--resume`)
|
|
365
385
|
|
|
366
386
|
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.
|
|
@@ -372,7 +392,7 @@ npx lacspace-leads cafes --city Kathmandu \
|
|
|
372
392
|
# crashes after Patan? just run it again — Thamel/Baneshwor/Patan are skipped.
|
|
373
393
|
```
|
|
374
394
|
|
|
375
|
-
|
|
395
|
+
For multi-search sweeps and `--config` campaigns, `--resume` uses the checkpoint. For a single search or a `--target` run (since 1.8.0), it continues from the rows already in the output file. The checkpoint key normalises case/whitespace, so the same area sweep always lines up.
|
|
376
396
|
|
|
377
397
|
## Run summary (`--summary`)
|
|
378
398
|
|
|
@@ -565,7 +585,8 @@ const leads = await searchLeadsBatch(
|
|
|
565
585
|
| `enrichContacts(website)` / `extractEmails` / `extractSocials` | Website enrichment, on tap. |
|
|
566
586
|
| `cleanWebsite` / `normalizePhone` / `sortLeads` | Pure data-cleaning helpers (unit-tested). |
|
|
567
587
|
| `haversineMeters` / `parseLatLngPair` / `parseDistance` | Pure geo helpers for radius search. |
|
|
568
|
-
| `filterLeads` / `dedupeLeads` / `subtractLeads` | Pure post-processing — filter, in-list dedupe, and cross-file dedupe. |
|
|
588
|
+
| `filterLeads` / `dedupeLeads` / `subtractLeads` / `dedupeKey` | Pure post-processing — filter, in-list dedupe, and cross-file dedupe. |
|
|
589
|
+
| `createLiveWriter(options)` | Write leads to csv / ndjson / json / xlsx (or stdout) one by one as they arrive. |
|
|
569
590
|
| `summarize(leads)` / `formatSummary(s)` | Rating bands, contact coverage %, top categories — the `--summary` engine. |
|
|
570
591
|
| `parsePriceLevel` / `priceLevelValue` / `parseBusinessStatus` / `parseClaimed` / `parseOpenNow` / `parseCategoryTags` | Pure field parsers (unit-tested against HTML/aria snippets). |
|
|
571
592
|
| `pipeToEnrich(handler)` / `leadsToEnrichInput` / `leadDomains` / `leadDomain` | Bridge collected leads to `lacspace-enrich` (no hard dep). |
|
|
@@ -573,7 +594,21 @@ const leads = await searchLeadsBatch(
|
|
|
573
594
|
| `expandQueries` / `resolvePreset` / `FIELD_PRESETS` | Batch expansion + field presets. |
|
|
574
595
|
| `composeQuery` / `mapsSearchUrl` / `normalizeFields` / `defaultFilename` | Query + helper utilities. |
|
|
575
596
|
|
|
576
|
-
|
|
597
|
+
Write leads as they're found (1.8.0):
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
import { searchLeads, createLiveWriter, DEFAULT_FIELDS } from "lacspace-leads";
|
|
601
|
+
|
|
602
|
+
const writer = createLiveWriter({ file: "cafes.csv", format: "csv", fields: [...DEFAULT_FIELDS] });
|
|
603
|
+
const leads = await searchLeads({
|
|
604
|
+
type: "cafes", city: "Kathmandu", limit: 100, headless: true,
|
|
605
|
+
onResult: (lead) => writer.add(lead), // a finished row, the moment it's ready
|
|
606
|
+
remaining: () => 100 - writer.added, // stop opening listings once there are 100
|
|
607
|
+
});
|
|
608
|
+
writer.finish(leads);
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
`onResult` gets each lead once it is final (enriched, verified, normalised, de-duplicated, filtered). `onLead` still gets the raw listing straight off the page, and `onProgress` streams status messages. `searchLeadsBatch` and target sweeps forward `onResult` across every search and tile, each business once. Everything is fully typed (`Lead`, `LeadField`, `SearchOptions`, `LeadStats`, `EmailStatus`, `SortKey`, `OutputFormat`, `Contacts` …) and ships dual **ESM + CJS**.
|
|
577
612
|
|
|
578
613
|
## Please use it responsibly
|
|
579
614
|
|