Build lead lists of local businesses from map and directory data: plan area and category searches that beat per-query result caps, qualify each business from public listing signals, find a reachable contact path from the listing to the business itself, and run list quality checks before outreach. Use when someone asks for local business leads, a list of businesses in an area by category, prospecting for a local offer, or turning map results into a clean outreach list. For B2B account research by ICP use lead_intelligence, for scoring inbound leads use lead_scoring, for managing your own Google Business Profile use google_business_profile.
name: local_lead_prospecting description: Build lead lists of local businesses from map and directory data: plan area and category searches that beat per-query result caps, qualify each business from public listing signals, find a reachable contact path from the listing to the business itself, and run list quality checks before outreach. Use when someone asks for local business leads, a list of businesses in an area by category, prospecting for a local offer, or turning map results into a clean outreach list. For B2B account research by ICP use lead_intelligence, for scoring inbound leads use lead_scoring, for managing your own Google Business Profile use google_business_profile.
Local Lead Prospecting
Purpose
Turn "find me plumbers in Birmingham" into a list a person can actually work: every row is a real, operating business in the target category and area, qualified against criteria written before the search, with a contact path that was seen on a public source and a note saying where each fact came from.
Local lists fail in predictable ways. One broad search returns only the most prominent slice of an area. The same business appears three times under spelling variants. Half the rows have no way to reach anyone. A list built from a source whose terms forbid storing the data creates legal risk for the person who uses it. This skill controls all four.
Workflow
1. Write the brief before searching
- State the offer in one sentence: what is sold, to which kind of local business, and what a good fit looks like.
- Define the target as category plus area plus size signals. Example: independent coffee shops, one or two locations, within the Birmingham metro, operating at least a year.
- Write the disqualify list the same day: chains above a location count you set, permanently closed businesses, categories that only look adjacent, businesses outside the service area, and any business already a customer or already contacted.
- Decide which qualification signals matter for this offer and write the thresholds down. Step 6 and references/qualification-rubric.md give a starting rubric. Thresholds set after seeing the data are decoration, not criteria.
2. Plan coverage so the area is actually covered
- Treat every map or directory search as a capped sample, not a census. Google Places API (New) Text Search pages with pageSize and nextPageToken and returns a limited set per query, so one query for a whole city returns the prominent businesses and misses the rest.
- Split the area into cells before searching: neighborhoods, ZIP or postal codes, suburbs, or a grid of center points with a radius per cell. Run one category query per cell.
- Keep a coverage log: cell, query text, source, date, raw results, new results after dedupe. A cell that returns far fewer results than its neighbors is a gap to re-run with a synonym category term, not a cell with no businesses.
- Use category synonyms deliberately. "Coffee shop", "cafe", and "espresso bar" can return different sets from the same source. Log which term produced each row.
- Stop expanding when new cells add mostly duplicates. Record the coverage you reached honestly: cells searched out of cells planned, not a claim that every business in the area was found.
3. Pick lawful sources, and respect what each one allows
Full detail is in references/sources-and-terms.md. The short version:
- Google Places API (New) is the usual primary source. Use the official API, Text Search for discovery and Place Details to refresh one business. Send an explicit X-Goog-FieldMask with only the fields the job needs, since the mask decides both the fields returned and the billing tier. Never scrape the Google Maps website or app in place of the API.
- Google Maps Platform terms restrict storing and reusing Google Maps Content outside the services, including bulk download of places information and copying business names, addresses, and reviews into an outside database. Place IDs are exempt from the caching restrictions, so store the place ID as the durable key, refresh the rest of the record from the API when you use it, keep attribution, and check the current terms before building anything that keeps Google data long term. If a use does not fit the terms, use a source that permits it instead.
- OpenStreetMap is the open alternative. Data is under the ODbL with attribution to OpenStreetMap contributors. The public Nominatim service allows at most 1 request per second with an identifying User-Agent, asks clients to cache results, and does not allow bulk geocoding. Public Overpass instances are shared and rate limited. For bulk area work, load a downloaded area extract and query it locally.
- Yelp Fusion and other directories are usable through their official APIs and within their own terms and rate limits. Directory data varies by category and city, so run it as a second source to fill gaps, not as a silent replacement.
- The business's own website is a primary source for contact paths and services. A chamber of commerce roster, a trade association directory, or a public licensing list is a strong cross-check for categories that need a license, because it confirms the business exists outside one map platform.
- Never buy a list whose seller cannot say where the data came from and when it was collected. Provenance you cannot state is a row you cannot defend.
4. Collect a fixed field set per business
Collect the same fields for every row, with a source and a collection date per row:
- Business name as listed, primary category and all listed types, street address, city, area cell it was found in.
- Place ID or the source's own ID, plus the listing URL.
- Business status. Anything not operating is out at the gate in step 6.
- Phone number as listed, website URL, rating, review count, and listed opening hours where the source returns them.
- Contact path fields from step 7, filled in a separate pass so discovery stays fast.
- A notes field for observable facts only: services named on the website, a second location, a booking link. Observations go in notes. Guesses do not go anywhere.
Request only the fields you will use. On Places API (New), phone, website, rating, and hours sit in higher field groups than name and address, so a wide mask on a discovery pass wastes money. Run discovery on a lean mask, then run Place Details only on rows that pass the first screen.
5. Dedupe by identity, not by name string
Run dedupe in this order:
- Exact match on place ID or source ID. Same ID is the same business location.
- Exact match on normalized phone number.
- Match on normalized name plus street address: lowercase, punctuation removed, common suffixes standardized (St, Street), unit numbers kept.
- Near match review: same name and same street, or same address and a similar name. A person or a careful review pass decides these. Do not auto-merge them.
Two branches of the same chain at different addresses are two locations, not duplicates. Keep both and tag them with the same brand name. A business that moved shows up twice with different addresses: keep the address that its own website lists, and mark the other row superseded with the date you checked.
The bundled scorer in step 6 also runs the first three dedupe passes on a CSV. Use bin/score_leads.py in this skill directory.
6. Qualify from public signals
Apply hard gates first, then score what survives. A row that fails a gate is out no matter how high it would score.
Hard gates:
- Business status is operating. Temporarily or permanently closed is out.
- Category matches the brief, or the business's own website clearly offers the target service.
- The business is inside the service area.
- At least one working contact path exists from step 7. A great fit nobody can reach is a research note, not a lead.
Scoring, out of 100, in three bands. The full rubric with worked numbers is in references/qualification-rubric.md:
- Fit, 0 to 40: category match, independent or small chain as the brief defines it, size signals such as review count in the range local customers produce, services on the website matching the offer.
- Reachability, 0 to 30: a published business phone, a website with a contact route, a contact form, a linked social profile. More than one path scores higher because one path often fails.
- Need signal, 0 to 30: observable gaps the offer fixes. No website at all. A website with no online booking in a category where booking is normal. Very few reviews compared with direct competitors in the same cell. Listed hours missing or incomplete. Score only what you can point to on a public page. A low rating alone is a weak need signal and never a reason to mock a business in outreach.
Rank by total score. Work the list top down, and keep the score and the reasons on the row so the person doing outreach can see why this business is on the list.
Run the scorer:
python3 bin/score_leads.py leads.csv --out ranked.csv --summary summary.json
python3 bin/score_leads.py --self-test
The scorer reads the CSV schema in references/qualification-rubric.md, applies the gates, dedupes, scores, and writes the ranked list plus a JSON summary with gate failures and duplicate counts. It does not fetch anything. Collect the data first, score second.
7. Find the contact path, from the listing to a person who can say yes
Work the ladder in order and stop at the first path that is real and published for business contact. Detail and scripts for the awkward cases are in references/contact-path.md.
- The business website: contact page, a published business email, a contact form, an online booking route, the phone number in the header or footer.
- A social profile linked from the business's own website. The link from the website is what proves the profile belongs to the business. A same-name profile found by search alone is unconfirmed.
- The listing itself: the phone number and website on the map or directory record, called or used during the business's listed hours.
- A named owner or manager on the business's own site, an About page, or a local press piece the business links to. Record the name and role only when a public source states them.
Rules for this step:
- A published business inbox such as a contact or bookings address the business lists for customers is a business contact path. A personal email address is not, even when it is easy to find.
- Never guess an email from a naming pattern and record it as a contact. A guessed address is inference. If a pattern guess is used at all in outreach tooling, label it as guessed on the row and verify it before any send.
- Never take contact details from review profiles, reviewers, or customers. They are not the business's contacts.
- For a sole trader, the business phone may also be a personal phone. Treat it with personal data care: business hours only, one clear opt-out honored at once, no repeated attempts across channels.
- Record the path, not just the value: "contact form on website, checked 2026-10-13" beats a bare URL, because paths rot and the next person needs to know how old the check is.
8. Verify the list before anyone works it
- Re-check a sample: at least 10 percent of rows, picked at random. For each, confirm the business is operating, the phone or website resolves, and the category is right. If more than 1 in 10 sampled rows is wrong, re-check the whole batch from that source or cell. Sampling math only works if the sample is random, so do not sample the top rows.
- Confirm every row has: name, category, area, at least one contact path with a checked date, source, score, and gate result.
- Check the list against suppression: current customers, anyone who opted out of outreach before, anyone already in an active conversation, and competitors if the brief excludes them.
- Count what you have honestly: rows collected, rows removed as duplicates, rows gated out by reason, rows ready. A list of 80 verified rows is worth more than 400 unworked rows, and the report should say which one this is.
9. Hand off in a form outreach can use
- Deliver the ranked CSV with the schema in the Output Contract, the coverage log, and the quality counts from step 8.
- Flag the top rows with the strongest need signal and write the one observable fact that supports it. That fact is what honest outreach references.
- Note which rows came from which source and which source terms apply, so nobody republishes a list in a way its source forbids.
Output Contract
A local prospecting run delivers:
- A ranked CSV with one row per business location and these columns: business_name, category, address, city, cell, source, source_id, listing_url, phone, website, rating, review_count, business_status, contact_path_type, contact_path, contact_checked_date, fit_score, reach_score, need_score, total_score, gate_result, need_signal_note, collected_date.
- A coverage log: one line per cell with query, source, date, raw count, and new count after dedupe.
- A quality summary: rows collected, duplicates removed, rows gated out by reason, sample verification result, and the count of rows ready for outreach.
- A source note: which sources were used, the date each was checked, and any term or licence condition that limits how the list may be stored or shared.
Operating Rules
- The API is the route, never scraping a map or directory website against its terms. When a source's terms do not allow the planned use, change the plan or change the source.
- Place IDs and source IDs are the durable keys. Other listing data goes stale and gets refreshed from the source before outreach, not trusted from an old export.
- Every row carries its source and collection date. A row without provenance gets verified or cut.
- Qualification criteria are written before collection and applied as written. Moving a threshold mid-run to hit a list size is how bad lists happen. Report a short list honestly instead.
- Score only observable public signals, and keep the evidence note on the row. No invented headcounts, revenue guesses, or owner names a source did not state.
- Business contact paths only, published by the business for contact. No personal emails, no reviewer data, no guessed addresses recorded as fact.
- Outreach that uses the list still has to follow the law where the recipient is. In the US, CAN-SPAM covers business to business commercial email: accurate headers, a non-deceptive subject line, clear ad identification, a valid physical postal address, a clear opt-out, and opt-outs honored within 10 business days. Other countries can require prior consent for some recipients. Check the rules for the recipient's country before sending, and honor every opt-out across the whole list at once.
- Respect rate limits and shared infrastructure. Public OpenStreetMap services are community run: 1 request per second on Nominatim, identifying User-Agent, cache results, and bulk work on a local extract. Paid APIs get the same discipline: lean field masks, logged request counts, no retry loops against a failing endpoint.
- Rating and review data describe the listing, not the worth of the people running the business. Use them as market signals on the row, never as material to shame a business in outreach.
- When two sources disagree on whether a business is operating, its own website and a working phone are the tiebreak. Mark the row disputed until one of them confirms it.
No ratings yet — be the first to rate it.