google-maps-list-builder について
google-maps-list-builderは、検索語と場所を指定してGoogle Mapsの店舗情報を収集し、CSVに出力するNode.jsツールです。企業情報を必要とする営業リスト作成向けで、ZIPコードによる絞り込み、重複排除、リクエスト制御に対応します。
主な機能
- 01検索語と場所による店舗検索
- 02店舗情報のCSV出力
- 03同梱の米国ZIPコードデータの絞り込み
- 04place_idによる重複排除
- 05リクエスト制御と再試行
活用シーン
- 01地域企業の見込み客リスト作成
- 02指定ZIPコード範囲での企業検索
- 03選別用企業データの準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ growthenginenowoslawski/ coldoutboundskills --skill google-maps-list-builderインストール後、Claude Code で/google-maps-list-builderを入力して読み込まれたか確認します。
詳細
Google Maps List Builder
Google Maps からビジネス情報をスクレイピングする、単独で動作するツールです。検索クエリ(例:「ピザレストラン」)と場所(郵便番号、市区町村、または座標)を指定すると、該当するすべてのビジネスの構造化データが CSV に出力されます。
コールドメールのワークフローにおける位置づけ
Google Maps から取得できるのは会社情報(名前、ドメイン、電話番号、住所、評価)です。人物情報は取得できません。コールドメールを実施するには、次の手順で進めます。
- このスキルを実行する →
company_domainを含むビジネスの CSV を作成 - 50件のサンプルに
/icp-prompt-builderを実行する — 下流のエンリッチメントに費用をかける前に、不適合な候補を除外するための適格性確認プロンプトを調整 - 絞り込み済みの CSV を使って
/blitz-list-builderを実行する → 各ビジネスのオーナー/マネージャーを追加 /list-builder(フェーズ 5、メール)を実行する → 不足しているメールアドレスを補完/cold-email-starter-kitのsmartlead-add-leads.tsを実行する → Smartlead にアップロード
このスキルは最初のステップにすぎません。
必須ステップ:/icp-prompt-builder で適格性を確認する
これは必須ステップです。省略しないでください。
Google Maps では「イリノイ州のピザレストラン」を喜んで 10,000 件返しますが、その大半は実際の ICP に合致しません(たとえば、従業員数 50~200 人の店舗だけが対象、またはオンライン注文に対応していない店舗だけが対象など)。エンリッチメントに費用をかける前に、約 50 件をサンプルとして抽出し、/icp-prompt-builder を実行してください。
- AI の適格性確認プロンプトで 10 件を評価する
- 「これは NO にすべきです。チェーンのフランチャイズです」と指摘する
- プロンプトを調整して、次の 10 件を実行する
- 2 回続けて修正が不要になるまで繰り返す
- 調整済みのプロンプトを適用して、残りのスクレイピング結果を絞り込む
必須である理由: /blitz-list-builder による後続のオーナー情報の検索とメールアドレスのウォーターフォール処理には、連絡先 1 件あたり $0.10~$0.30 かかります。ビジネスを 10,000 件スクレイピングした場合、費用は $1K~$3K になります。事前に適格性を確認することで、平均してこの費用を 50~80%削減できます。
開始前に必要なもの
- Node.js 18+ と npm がインストールされていること
- RapidAPI アカウント(無料プランあり)と Maps Data API のサブスクリプション:
- https://rapidapi.com で登録する
- API をサブスクライブする:https://rapidapi.com/alexanderxbx/api/maps-data
- ダッシュボードから RapidAPI キーをコピーする(各エンドポイントのページにある
X-RapidAPI-Keyヘッダーに記載されています)
これだけです。Google Cloud アカウント、OAuth、RapidAPI 以外の課金設定は必要ありません。
プロジェクトのセットアップ
新しいプロジェクトディレクトリを作成して初期化します。
mkdir google-maps-scraper && cd google-maps-scraper
npm init -y
npm install typescript bottleneck
npm install -D @types/node tsx
package.json のスクリプトに追加します:
{
"scripts": {
"scrape": "tsx src/index.ts"
}
}
tsconfig.json を作成します:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"]
}
API キーを環境変数として設定します:
export RAPIDAPI_KEY=your_key_here
または、.env ファイルを作成します(.env を .gitignore に追加してください):
RAPIDAPI_KEY=your_key_here
ファイル構成
google-maps-scraper/
data/
us-zip-codes.csv # 42,734 US zip codes with city, state, lat/lng, population
src/
index.ts # CLI entry point
client.ts # RapidAPI Maps Data client with rate limiting
types.ts # TypeScript interfaces
csv.ts # CSV export
zips.ts # Zip code loader (filter by state, city, population)
同梱の郵便番号データベース
リポジトリには data/us-zip-codes.csv が含まれています。これは 42,734 件のエントリーを含む米国の完全な郵便番号データです。列は次のとおりです。
zip,primary_city,state,timezone,area_codes,world_region,country,latitude,longitude,irs_estimated_population
このデータを使えば、郵便番号を手作業で列挙することなく、州全体や都市圏をスクレイピングできます。src/zips.ts のローダーでは、州、市区町村、最低人口数で絞り込めます。
コアファイル
src/types.ts
export interface SearchParams {
query: string; // "pizza restaurant", "dentist", "gym"
lat?: number; // Center latitude (optional if using "query in zipcode" format)
lng?: number; // Center longitude
limit?: number; // Max results per search (default 20, max 20)
zoom?: number; // Map zoom level (default 13 = neighborhood)
country?: string; // Country code (default "us")
}
export interface Place {
place_id: string;
name: string;
address: string;
lat: number;
lng: number;
rating?: number;
reviews_count?: number;
phone?: string;
website?: string;
types?: string[];
category?: string;
}
export interface ScrapeResult {
query: string;
location: string;
total_results: number;
unique_results: number;
places: Place[];
duration_ms: number;
}
src/zips.ts
同梱の郵便番号 CSV を読み込み、絞り込みます。州、市区町村名、最低人口数を指定して対象を絞れます。
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
export interface ZipEntry {
zip: string;
city: string;
state: string;
lat: number;
lng: number;
population: number;
}
let cache: ZipEntry[] | null = null;
function loadAll(): ZipEntry[] {
if (cache) return cache;
const __dirname = dirname(fileURLToPath(import.meta.url));
const csvPath = join(__dirname, '..', 'data', 'us-zip-codes.csv');
const raw = readFileSync(csvPath, 'utf-8');
const lines = raw.trim().split('\n').slice(1); // skip header
cache = lines.map(line => {
// Handle quoted fields (area_codes can contain commas)
const parts: string[] = [];
let current = '';
let inQuotes = false;
for (const ch of line) {
if (ch === '"') { inQuotes = !inQuotes; continue; }
if (ch === ',' && !inQuotes) { parts.push(current); current = ''; continue; }
current += ch;
}
parts.push(current);
return {
zip: parts[0]?.padStart(5, '0') || '',
city: parts[1] || '',
state: parts[2] || '',
lat: parseFloat(parts[7]) || 0,
lng: parseFloat(parts[8]) || 0,
population: parseInt(parts[9]) || 0,
};
}).filter(z => z.zip.length === 5);
return cache;
}
/** Get zips for a US state (2-letter code, e.g. "CA", "TX") */
export function getZipsByState(stateCode: string): ZipEntry[] {
return loadAll().filter(z => z.state.toUpperCase() === stateCode.toUpperCase());
}
/** Get zips for a city name (case-insensitive, partial match) */
export function getZipsByCity(city: string, state?: string): ZipEntry[] {
const cityLower = city.toLowerCase();
return loadAll().filter(z => {
const cityMatch = z.city.toLowerCase().includes(cityLower);
const stateMatch = !state || z.state.toUpperCase() === state.toUpperCase();
return cityMatch && stateMatch;
});
}
/** Get zips with population above a threshold */
export function getZipsByMinPopulation(minPop: number, state?: string): ZipEntry[] {
return loadAll().filter(z => {
const popMatch = z.population >= minPop;
const stateMatch = !state || z.state.toUpperCase() === state.toUpperCase();
return popMatch && stateMatch;
});
}
/** Get all loaded zip entries */
export function getAllZips(): ZipEntry[] {
return loadAll();
}
src/client.ts
これはコア API クライアントです。レート制限(毎秒 2 リクエスト)と、指数バックオフによる再試行を処理します。
import Bottleneck from 'bottleneck';
import type { SearchParams, Place } from './types.js';
interface RawSearchResponse {
data?: Array<{
place_id?: string;
title?: string;
name?: string;
address?: string;
latitude?: number;
longitude?: number;
rating?: number;
reviews?: number;
phone?: string;
website?: string;
types?: string[];
type?: string;
category?: string;
}>;
error?: string;
}
interface GeocodingResponse {
latitude?: number;
longitude?: number;
formatted_address?: string;
error?: string;
}
export class GoogleMapsClient {
private limiter: Bottleneck;
private apiKey: string;
private host = 'maps-data.p.rapidapi.com';
private maxRetries: number;
constructor(opts: { apiKey: string; requestsPerSecond?: number; maxRetries?: number }) {
this.apiKey = opts.apiKey;
this.maxRetries = opts.maxRetries ?? 3;
this.limiter = new Bottleneck({
maxConcurrent: 1,
minTime: Math.floor(1000 / (opts.requestsPerSecond ?? 2)),
});
}
/** Search Google Maps for businesses */
async search(params: SearchParams): Promise<Place[]> {
const response = await this.request<RawSearchResponse>('searchmaps.php', {
query: params.query,
limit: String(params.limit ?? 20),
country: params.country ?? 'us',
...(params.lat != null && { lat: String(params.lat) }),
...(params.lng != null && { lng: String(params.lng) }),
...(params.zoom != null && { zoom: String(params.zoom) }),
});
if (response.error) throw new Error(`Search failed: ${response.error}`);
return this.transform(response.data || []);
}
/** Geocode a zip code or address to lat/lng */
async geocode(query: string, country = 'us'): Promise<{ lat: number; lng: number }> {
const response = await this.request<GeocodingResponse>('geocoding.php', {
query: `${query}, ${country.toUpperCase()}`,
});
if (!response.latitude || !response.longitude) {
throw new Error(`Could not geocode: ${query}`);
}
return { lat: response.latitude, lng: response.longitude };
}
private async request<T>(endpoint: string, params: Record<string, string>): Promise<T> {
return this.limiter.schedule(() => this.requestWithRetry<T>(endpoint, params));
}
private async requestWithRetry<T>(
endpoint: string,
params: Record<string, string>,
attempt = 0
): Promise<T> {
const url = new URL(`https://${this.host}/${endpoint}`);
for (const [k, v] of Object.entries(params)) {
if (v != null) url.searchParams.set(k, v);
}
try {
const res = await fetch(url.toString(), {
headers: {
'X-RapidAPI-Key': this.apiKey,
'X-RapidAPI-Host': this.host,
},
});
if (!res.ok) {
const err: any = new Error(`API ${res.status}: ${res.statusText}`);
err.statusCode = res.status;
throw err;
}
return (await res.json()) as T;
} catch (err: any) {
const retryable =
attempt < this.maxRetries &&
(err.statusCode === 429 || err.statusCode >= 500 ||
err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT');
if (retryable) {
const delay = 1000 * Math.pow(2, attempt);
console.log(` Retry ${attempt + 1}/${this.maxRetries} in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
return this.requestWithRetry<T>(endpoint, params, attempt + 1);
}
throw err;
}
}
private transform(data: NonNullable<RawSearchResponse['data']>): Place[] {
return data.map(item => ({
place_id: item.place_id || '',
name: item.title || item.name || '',
address: item.address || '',
lat: item.latitude || 0,
lng: item.longitude || 0,
rating: item.rating,
reviews_count: item.reviews,
phone: item.phone,
website: item.website,
types: item.types || (item.type ? [item.type] : []),
category: item.category || item.type,
}));
}
}
src/csv.ts
import { writeFile, mkdir } from 'fs/promises';
import { dirname } from 'path';
import type { Place } from './types.js';
const HEADERS = [
'place_id', 'name', 'address', 'phone', 'website',
'rating', 'reviews_count', 'lat', 'lng', 'category',
];
function escape(val: string | number | undefined | null): string {
if (val == null) return '';
const s = String(val);
return s.includes(',') || s.includes('"') || s.includes('\n')
? `"${s.replace(/"/g, '""')}"`
: s;
}
export async function exportCSV(places: Place[], outputPath: string): Promise<void> {
await mkdir(dirname(outputPath), { recursive: true });
const lines = [
HEADERS.join(','),
...places.map(p =>
HEADERS.map(h => escape(p[h as keyof Place])).join(',')
),
];
await writeFile(outputPath, lines.join('\n') + '\n', 'utf-8');
}
src/index.ts
import { GoogleMapsClient } from './client.js';
import { exportCSV } from './csv.js';
import { getZipsByState, getZipsByCity, getZipsByMinPopulation } from './zips.js';
import type { Place } from './types.js';
function dedup(places: Place[]): Place[] {
const seen = new Set<string>();
return places.filter(p => {
if (seen.has(p.place_id)) return false;
seen.add(p.place_id);
return true;
});
}
function getArg(args: string[], prefix: string): string | undefined {
const match = args.find(a => a.startsWith(prefix));
return match ? match.split('=').slice(1).join('=') : undefined;
}
async function main() {
const args = process.argv.slice(2);
const query = getArg(args, '--query=');
const zips = getArg(args, '--zips=');
const cities = getArg(args, '--cities=');
const state = getArg(args, '--state=');
const minPop = getArg(args, '--min-pop=');
const limit = parseInt(getArg(args, '--limit=') || '20', 10);
const output = getArg(args, '--output=') || './output/results.csv';
if (!query || (!zips && !cities && !state)) {
console.log(`
Google Maps Scraper
USAGE:
npm run scrape -- --query="pizza restaurant" --zips=10014,10013,10012
npm run scrape -- --query="dentist" --state=TX
npm run scrape -- --query="dentist" --state=TX --min-pop=10000
npm run scrape -- --query="gym" --cities="Austin TX,Dallas TX"
OPTIONS:
--query=QUERY Business type to search for (required)
--zips=ZIP,ZIP Comma-separated zip codes to search
--cities=CITY,CITY Comma-separated cities to search
--state=XX Search all zip codes in a US state (2-letter code)
--min-pop=N Filter zips to those with population >= N (use with --state)
--limit=N Max results per location (default 20, max 20)
--output=PATH Output CSV path (default ./output/results.csv)
ENVIRONMENT:
RAPIDAPI_KEY Your RapidAPI key (required)
Get one at: https://rapidapi.com/alexanderxbx/api/maps-data
EXAMPLES:
# All pizza places in California (zips with pop >= 5000)
npm run scrape -- --query="pizza restaurant" --state=CA --min-pop=5000
# Dentists in specific NYC zip codes
npm run scrape -- --query="dentist" --zips=10014,10013,10012
# Gyms across Texas cities
npm run scrape -- --query="gym" --cities="Austin TX,Dallas TX,Houston TX"
`);
process.exit(1);
}
const apiKey = process.env.RAPIDAPI_KEY;
if (!apiKey) {
console.error('Error: RAPIDAPI_KEY environment variable is required');
console.error('Get your key at: https://rapidapi.com/alexanderxbx/api/maps-data');
process.exit(1);
}
// Build location list from all sources
const locations: string[] = [];
if (zips) {
locations.push(...zips.split(',').map(s => s.trim()));
}
if (cities) {
locations.push(...cities.split(',').map(s => s.trim()));
}
if (state) {
const minPopNum = minPop ? parseInt(minPop, 10) : 0;
const stateZips = minPopNum > 0
? getZipsByMinPopulation(minPopNum, state)
: getZipsByState(state);
locations.push(...stateZips.map(z => z.zip));
console.log(`Loaded ${stateZips.length} zip codes for ${state.toUpperCase()}${minPopNum > 0 ? ` (pop >= ${minPopNum.toLocaleString()})` : ''}`);
}
if (locations.length === 0) {
console.error('No locations to search. Provide --zips, --cities, or --state.');
process.exit(1);
}
console.log(`Scraping "${query}" across ${locations.length} location(s)...\n`);
const client = new GoogleMapsClient({ apiKey, requestsPerSecond: 2 });
const allPlaces: Place[] = [];
const start = Date.now();
for (let i = 0; i < locations.length; i++) {
const loc = locations[i];
console.log(` [${i + 1}/${locations.length}] Searching: ${query} in ${loc}`);
try {
const results = await client.search({
query: `${query} in ${loc}`,
limit,
country: 'us',
});
allPlaces.push(...results);
console.log(` Found ${results.length} results`);
} catch (err: any) {
console.error(` Error: ${err.message}`);
}
}
// Deduplicate
const unique = dedup(allPlaces);
console.log(`\nTotal: ${allPlaces.length} results, ${unique.length} unique after dedup`);
// Export
await exportCSV(unique, output);
console.log(`Saved to: ${output}`);
const duration = ((Date.now() - start) / 1000).toFixed(1);
console.log(`Done in ${duration}s`);
// Print sample
if (unique.length > 0) {
console.log('\nSample result:');
const sample = unique[0];
console.log(` ${sample.name}`);
console.log(` ${sample.address}`);
if (sample.phone) console.log(` ${sample.phone}`);
if (sample.website) console.log(` ${sample.website}`);
if (sample.rating) console.log(` ${sample.rating} stars (${sample.reviews_count} reviews)`);
}
}
main().catch(err => {
console.error('Fatal:', err.message);
process.exit(1);
});
実行方法
ローカル CLI
# Search pizza places in 3 NYC zip codes
npm run scrape -- --query="pizza restaurant" --zips=10014,10013,10012
# Search ALL dentists in Texas (zips with population >= 5000)
npm run scrape -- --query="dentist" --state=TX --min-pop=5000
# Every gym in California (all 2,657 zip codes — takes a while)
npm run scrape -- --query="gym" --state=CA
# Search dentists across specific cities
npm run scrape -- --query="dentist" --cities="Austin TX,Dallas TX,San Antonio TX"
# Custom output file
npm run scrape -- --query="gym" --zips=90210 --output=./data/gyms.csv
プログラムからの使用
import { GoogleMapsClient } from './client.js';
const client = new GoogleMapsClient({
apiKey: process.env.RAPIDAPI_KEY!,
requestsPerSecond: 2,
});
// Simple search
const places = await client.search({
query: 'coffee shop in 94105',
limit: 20,
});
// Search with coordinates
const { lat, lng } = await client.geocode('94105');
const nearby = await client.search({
query: 'coffee shop',
lat,
lng,
zoom: 14,
limit: 20,
});
Web アプリとしてデプロイする(任意)
CLI の代わりに、または CLI に加えてブラウザー UI を使いたい場合は、Express を追加します。
npm install express
npm install -D @types/express
src/server.ts を作成します。
import express from 'express';
import { GoogleMapsClient } from './client.js';
import { exportCSV } from './csv.js';
import { tmpdir } from 'os';
import { join } from 'path';
import { readFile, unlink } from 'fs/promises';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const client = new GoogleMapsClient({
apiKey: process.env.RAPIDAPI_KEY!,
requestsPerSecond: 2,
});
// Simple HTML form
app.get('/', (_req, res) => {
res.send(`<!DOCTYPE html>
<html><head><title>Google Maps Scraper</title></head>
<body style="font-family:sans-serif;max-width:600px;margin:40px auto;padding:0 20px">
<h1>Google Maps Scraper</h1>
<form method="POST" action="/scrape">
<label>Search query:<br>
<input name="query" placeholder="pizza restaurant" style="width:100%;padding:8px;margin:4px 0 12px" required>
</label>
<label>Locations (comma-separated zips or cities):<br>
<input name="locations" placeholder="10014, 10013, 10012" style="width:100%;padding:8px;margin:4px 0 12px" required>
</label>
<button type="submit" style="padding:10px 24px;cursor:pointer">Scrape</button>
</form>
</body></html>`);
});
app.post('/scrape', async (req, res) => {
const { query, locations: locStr } = req.body;
const locations = locStr.split(',').map((s: string) => s.trim()).filter(Boolean);
const allPlaces: any[] = [];
for (const loc of locations) {
try {
const results = await client.search({ query: `${query} in ${loc}`, limit: 20 });
allPlaces.push(...results);
} catch {}
}
// Dedup
const seen = new Set<string>();
const unique = allPlaces.filter(p => { if (seen.has(p.place_id)) return false; seen.add(p.place_id); return true; });
// Export CSV and send as download
const tmpPath = join(tmpdir(), `scrape-${Date.now()}.csv`);
await exportCSV(unique, tmpPath);
const csv = await readFile(tmpPath, 'utf-8');
await unlink(tmpPath);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="maps-scrape-${Date.now()}.csv"`);
res.send(csv);
});
const port = parseInt(process.env.PORT || '3000', 10);
app.listen(port, () => console.log(`Scraper running at http://localhost:${port}`));
package.json にスクリプトを追加します:
{
"scripts": {
"scrape": "tsx src/index.ts",
"serve": "tsx src/server.ts"
}
}
ローカルで実行します:npm run serve を実行してから、http://localhost:3000 を開きます。
Railway へのデプロイ
- プロジェクトを GitHub リポジトリにプッシュします
- https://railway.com にアクセスし、新しいプロジェクトを作成してリポジトリを接続します
- Railway のダッシュボードで環境変数
RAPIDAPI_KEYを設定します - 起動コマンドを
npx tsx src/server.tsに設定します - Railway は
process.env.PORTからポートを自動検出し、公開 URL を発行します
POST ハンドラーで PASSWORD 環境変数を確認するか、Railway の組み込み認証機能を使用すると、パスワード保護を追加できます。
他のプラットフォームへのデプロイ
これは標準的な Node.js アプリです。次の場所で実行できます。
- Render:GitHub リポジトリを接続し、環境変数を設定すれば完了です
- Fly.io:
fly launchを実行し、fly secrets set RAPIDAPI_KEY=xxxでシークレットを設定します - Vercel:サーバーレス関数としてデプロイします(ハンドラーをエクスポートするよう
server.tsを変更してください) - Docker:
FROM node:20-slim+npm install+npx tsx src/server.ts
API リファレンス
基盤となる API は、RapidAPI の Maps Data API です。 https://rapidapi.com/alexanderxbx/api/maps-data
使用する主なエンドポイント
| エンドポイント | 用途 | 例 |
|---|---|---|
searchmaps.php | クエリと場所でビジネスを検索 | ?query=pizza+in+10014&limit=20 |
geocoding.php | 住所/郵便番号を緯度/経度に変換 | ?query=10014,+US |
nearby.php | 緯度/経度の地点周辺を検索 | ?query=pizza&lat=40.73&lng=-74.00 |
place.php | 1 件のビジネスの詳細情報を取得 | ?place_id=ChIJ... |
レート制限
RapidAPI の無料プランにはリクエスト制限があります(利用中のプランを確認してください)。クライアントは毎秒 2 リクエストに固定されており、429 エラーが発生した場合は自動で再試行します。プランでより多くのリクエストが許可されている場合は、requestsPerSecond を調整してください。
レスポンスフィールド
各結果には次の情報が含まれます。
place_id— 一意の Google Maps 識別子name— ビジネス名address— 番地を含む住所phone— 電話番号(掲載されている場合)website— ウェブサイトの URL(掲載されている場合)rating— 星評価(1~5)reviews_count— Google のレビュー数lat/lng— 座標types/category— ビジネスのカテゴリ(例:「pizza_restaurant」)
ヒント
- 米国の検索では 「query in zipcode」形式 が最適です。座標は不要です。
- 検索あたりの上限は 20 件 です。より広くカバーするには、重複する複数の郵便番号を検索してください。
place_idで重複を除去してください。同じビジネスが隣接する郵便番号の検索結果に表示されることがよくあります。- 料理/カテゴリによる絞り込み:
typesフィールドでビジネスの種類がわかります。これを使って関連性のない結果を除外してください(例:「restaurant」の検索時に「bar」を除外する)。 - 費用:RapidAPI のプランを確認してください。通常、無料プランでテストには十分です。有料プランも大量のデータ取得には安価です。
次にすること
50 件のビジネスのサンプルに対して /icp-prompt-builder を実行してください(上記の必須手順)。次に、絞り込んだドメインを使って /blitz-list-builder を実行し、オーナーの連絡先を探します。Google Maps が返すのはビジネスであり、人ではありません。
オーナーの特定後、不足しているメールアドレスを補うために /list-builder(Phase 5、メールアドレス)を実行し、その後 /list-quality-scorecard で評価します。
**または、待つ:**スクレイピング結果が 200 件未満の場合、クエリと場所の範囲が狭すぎます。次に進む前に範囲を広げてください。
関連スキル
/icp-prompt-builder— 必須の適格性確認/blitz-list-builder— 各ビジネスのオーナーの連絡先を探す/list-builder(Phase 5、メールアドレス)— 不足しているメールアドレスを補う/list-quality-scorecard— 最終リストを評価する