A useful business list is narrow, de-duplicated, and honest about what it doesn’t cover. This guide follows one worked example — independent coffee businesses in five Seattle neighborhoods — from brief to a spreadsheet your team can act on, using either the Online scraper or the API.
Step 1
Write a one-paragraph research brief
Answer four questions before you search. They decide your keywords, your filters, and which fields matter.
Who?
Where?
Qualified if…
Used for…
Step 2
Build the keyword grid
Combine every category with every area. Smaller areas return more complete coverage than one broad search, because each Google Maps search returns a bounded list of listings.
| Area ↓ / Category → | specialty coffee shops | coffee roasters |
|---|---|---|
| Capitol Hill | specialty coffee shops in Capitol Hill, Seattle | coffee roasters in Capitol Hill, Seattle |
| Ballard | specialty coffee shops in Ballard, Seattle | coffee roasters in Ballard, Seattle |
| Fremont | specialty coffee shops in Fremont, Seattle | coffee roasters in Fremont, Seattle |
| Queen Anne | specialty coffee shops in Queen Anne, Seattle | coffee roasters in Queen Anne, Seattle |
| Georgetown | specialty coffee shops in Georgetown, Seattle | coffee roasters in Georgetown, Seattle |
That’s 10 keywords — it fits in one scrape on the Basic Online plan (10 keywords per scrape), or two scrapes on Lite. Generate longer grids with a few lines of code:
const categories = ["specialty coffee shops","coffee roasters"];
const areas = ["Capitol Hill","Ballard","Fremont","Queen Anne","Georgetown"];
const keywords = categories.flatMap((category) =>
areas.map((area) => `${category} in ${area}, Seattle`),
);
console.log(keywords.join("\n")); // paste into New scrape, or submit via the APIStep 3
Run the searches
With the Online scraper
Step 1: Paste the grid into New scrape
One keyword per line, then select Start scraping. The job keeps running if you close the tab.
Step 2: Spot-check the first keyword
When the first keyword finishes, open View on its CSV. If results are off-target (for example lots of chains or bakeries), cancel, adjust the wording, and resubmit before spending the rest of your allowance.
Step 3: Download every keyword CSV
You’ll get
keyword-1.csv…keyword-10.csv, in the order of your grid.
With the API
Send one POST /search request per keyword. Each response returns up to 20 businesses and costs one request; use page: 2 through 10 for additional pages.
import { randomUUID } from "node:crypto";
// One search request per keyword. Save each idempotency key with its keyword so retries never duplicate work.
const tasks = keywords.map((q) => ({ q, idempotencyKey: randomUUID() }));
for (const task of tasks) {
const response = await fetch("https://gmapscrawl.com/api/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GMSCRAPER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": task.idempotencyKey,
},
body: JSON.stringify({
q: task.q, page: 1, gl: "us", extra: false,
}),
});
const { data, error } = await response.json();
if (error) throw new Error(`${task.q}: ${error.code}`);
task.businesses = data.businesses;
await new Promise((resolve) => setTimeout(resolve, 1_000)); // stay well under your rate limit
}Read data.businesses directly from each response, as shown in Run your first API search.
Step 4
Clean and qualify the list
Apply the brief in this order — it keeps the logic easy to review:
| # | Step | Why |
|---|---|---|
| 1 | Remove duplicates by Place Id (or Cid) | Neighboring areas overlap; the same shop can appear in several keyword files. |
| 2 | Drop closed or off-category businesses | Search results can include related categories — bakeries, grocery stores, chains. |
| 3 | Apply your qualification rules | Rating, review count, website — whatever your brief says. |
| 4 | Check contact coverage | Note which rows lack a phone or email rather than silently deleting them. |
// 1. Import each keyword CSV into a sheet named Raw (append rows under one header).
// 2. Data → Data cleanup → Remove duplicates → select the "Place Id" column.
// 3. In a new sheet, keep well-reviewed businesses that have a website:
=FILTER(Raw!A2:AJ, Raw!W2:W>=4, Raw!V2:V>=25, Raw!AC2:AC<>"")
// Count how many rows are missing a phone number:
=COUNTBLANK(Raw!N2:N)Step 5
Hand off with context
Before sharing the list, add the context someone else needs to trust it:
| Add | Example |
|---|---|
| Source keyword | specialty coffee shops in Ballard, Seattle |
| Job ID | job_4f1c… |
| Observed at | 2026-09-24T08:12:00Z |
| Coverage notes | “Georgetown keyword was partial — 2 of 10 keywords incomplete.” |
| Qualification rules | “Rating ≥ 4.0, ≥ 25 reviews, has website, independent only.” |
Plan to refresh the list periodically. Businesses open, close, and change phone numbers; the observedAt value tells you how old each record is.
Good practice
Use the list responsibly
- Collect only the fields your brief needs.
- Verify a contact’s source page before using it for outreach.
- Honor opt-outs and the email, calling, and privacy rules that apply where you and the business operate.
- Don’t use scraped review text or photos in ways that misrepresent the business.
- Read the public data notice and terms of service.