Google Maps B2B Lead Goldmine
by @nicemaths123
Extract, score, and export detailed local business leads from Google Maps by keyword and location with contact info, reviews, and personalized outreach messa...
Basic Lead Extraction by Keyword and City
import ApifyClient from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor("compass~crawler-google-places").call({
searchStringsArray: ["dentists in Miami, FL"],
maxCrawledPlacesPerSearch: 50,
language: "en",
includeWebResults: false
});
const { items } = await run.dataset().getData();
// Each item contains:
// { title, phone, website, address, totalScore, reviewsCount,
// categoryName, openingHours, email, location }
console.log(Found ${items.length} leads);
Multi-Location Parallel Search
const locations = [
"dentists in Miami, FL",
"dentists in Fort Lauderdale, FL",
"dentists in West Palm Beach, FL",
"dentists in Tampa, FL",
"dentists in Orlando, FL"
];const runs = await Promise.all(
locations.map(search =>
client.actor("compass~crawler-google-places").call({
searchStringsArray: [search],
maxCrawledPlacesPerSearch: 50,
language: "en"
})
)
);
const allLeads = [];
for (const run of runs) {
const { items } = await run.dataset().getData();
allLeads.push(...items);
}
// Deduplicate by phone number
const seen = new Set();
const unique = allLeads.filter(lead => {
if (!lead.phone || seen.has(lead.phone)) return false;
seen.add(lead.phone);
return true;
});
console.log(${unique.length} unique leads across ${locations.length} cities);
Lead Scoring Algorithm
function scoreLead(lead) {
let score = 50; // Review gap signal: few reviews = needs marketing help
if (lead.reviewsCount < 10) score += 20;
else if (lead.reviewsCount < 30) score += 10;
// Low rating signal: needs reputation management
if (lead.totalScore && lead.totalScore < 4.0) score += 15;
else if (lead.totalScore && lead.totalScore < 4.5) score += 5;
// No website = massive opportunity
if (!lead.website || lead.website === '') score += 25;
// Has website but no email listed = hard to reach
if (lead.website && !lead.email) score -= 5;
// Has phone = contactable
if (lead.phone) score += 5;
// Category bonus for high-value niches
const highValue = ['lawyer', 'dentist', 'doctor', 'real estate', 'contractor', 'plumber'];
if (highValue.some(k => (lead.categoryName || '').toLowerCase().includes(k))) {
score += 10;
}
return Math.min(100, Math.max(0, score));
}
const scored = unique.map(lead => ({
...lead,
leadScore: scoreLead(lead)
})).sort((a, b) => b.leadScore - a.leadScore);
console.log("Top 10 leads:");
scored.slice(0, 10).forEach((lead, i) => {
console.log(${i + 1}. [${lead.leadScore}/100] ${lead.title} | ${lead.phone} | ${lead.website || 'NO WEBSITE'});
});
Deep Email Extraction from Business Websites
async function extractEmails(leads) {
const withWebsites = leads.filter(l => l.website); const run = await client.actor("apify/website-content-crawler").call({
startUrls: withWebsites.slice(0, 20).map(l => ({ url: l.website })),
maxCrawlPages: 3,
crawlerType: "cheerio"
});
const { items } = await run.dataset().getData();
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
const enriched = items.map(page => {
const emails = [...new Set((page.text || '').match(emailRegex) || [])];
return { url: page.url, emails };
});
return enriched;
}
Generate Personalized Outreach per Lead
import axios from 'axios';;async function generateOutreach(lead) { const prompt =
Write a short cold email (under 80 words) for this local business.LEAD:
Business: ${lead.title} Category: ${lead.categoryName} Location: ${lead.address} Rating: ${lead.totalScore}/5 (${lead.reviewsCount} reviews) Website: ${lead.website || 'None'} Lead Score: ${lead.leadScore}/100 RULES:
Reference something specific about their business If they have few reviews, mention you can help them get more If they have no website, mention you can build one If their rating is below 4.5, mention reputation management Keep it conversational, no corporate speak End with a soft question, not a hard CTA Include [YOUR_NAME] and [YOUR_COMPANY] placeholders Return subject line and body only.
const { data } = await axios.post('https://api.anthropic.com/v1/messages', { model: "claude-sonnet-4-20250514", max_tokens: 250, messages: [{ role: "user", content: prompt }] }, { headers: { 'x-api-key': process.env.CLAUDE_API_KEY, 'anthropic-version': '2023-06-01' } });
return data.content[0].text; }
// Generate outreach for top 10 leads for (const lead of scored.slice(0, 10)) { lead.outreachEmail = await generateOutreach(lead); await new Promise(r => setTimeout(r, 500)); }
Full Pipeline: Search, Score, Enrich, Outreach, Export
import { writeFileSync } from 'fs';async function fullLeadPipeline(keyword, locations, maxPerLocation = 50) {
console.log(Starting pipeline for: ${keyword});
// STEP 1: Scrape all locations in parallel
const searches = locations.map(loc => ${keyword} in ${loc});
const runs = await Promise.all(
searches.map(s =>
client.actor("compass~crawler-google-places").call({
searchStringsArray: [s],
maxCrawledPlacesPerSearch: maxPerLocation,
language: "en"
})
)
);
let allLeads = [];
for (const run of runs) {
const { items } = await run.dataset().getData();
allLeads.push(...items);
}
// STEP 2: Deduplicate
const seen = new Set();
const unique = allLeads.filter(l => {
const key = l.phone || l.title;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
// STEP 3: Score
const scored = unique.map(l => ({ ...l, leadScore: scoreLead(l) }))
.sort((a, b) => b.leadScore - a.leadScore);
// STEP 4: Generate outreach for top 20
for (const lead of scored.slice(0, 20)) {
lead.outreachEmail = await generateOutreach(lead);
await new Promise(r => setTimeout(r, 500));
}
// STEP 5: Export to CSV
const headers = ["title","phone","email","website","address","totalScore","reviewsCount","categoryName","leadScore","outreachEmail"];
const csv = [
headers.join(","),
...scored.map(l => headers.map(h => "${(l[h] || '').toString().replace(/"/g, '""')}").join(","))
].join("\n");
const filename = leads-${keyword.replace(/\s+/g, '_')}-${Date.now()}.csv;
writeFileSync(filename, csv);
console.log(Exported ${scored.length} scored leads to ${filename});
return scored;
}
// Usage
await fullLeadPipeline("plumbers", ["Miami, FL", "Fort Lauderdale, FL", "Tampa, FL"]);
clawhub install google-maps-extractor