mcpField
G
google-maps-list-builder

google-maps-list-builder

github.com/growthenginenowoslawski营销与增长79073 次安装3 周前更新
代码仓库

关于 google-maps-list-builder

google-maps-list-builder 是一个 Node.js 工具,可按查询词和地点收集 Google Maps 商家信息并导出为 CSV,适用于需要公司线索的开发流程,支持邮编筛选、去重和限速请求。

主要功能

  1. 01按查询词和地点搜索商家
  2. 02将商家记录导出为 CSV
  3. 03筛选内置的美国邮编数据
  4. 04按 place_id 去重
  5. 05限速请求与自动重试

适用场景

  1. 01建立本地商家潜在客户名单
  2. 02按指定邮编查找商家
  3. 03准备待筛选的公司记录

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder

安装后在 Claude Code 中输入/google-maps-list-builder确认已加载。

详细介绍

Google Maps 列表构建器

一款独立工具,用于抓取 Google Maps 上的商家列表。提供搜索查询(例如“披萨餐厅”)和地点(邮政编码、城市或坐标),即可获取所有匹配商家的结构化数据,并写入 CSV。

在冷邮件流程中的作用

Google Maps 可提供公司信息(名称、域名、电话、地址、评分),但无法提供个人信息。要开展冷邮件营销:

  1. 运行此技能 → 获取包含 company_domain 的商家 CSV
  2. 对 50 条样本运行 /icp-prompt-builder——调整资格筛选提示词,在付费进行后续数据补充之前排除不合适的对象
  3. 使用筛选后的 CSV 运行 /blitz-list-builder → 为每家商户添加所有者/经理信息
  4. 运行 /list-builder(第 5 阶段,邮箱)→ 补全缺失的邮箱
  5. 运行 /cold-email-starter-kit 的 smartlead-add-leads.ts → 上传到 Smartlead

此技能只是第一步。

必需步骤:使用 /icp-prompt-builder 进行资格筛选

此步骤为必需步骤。请勿跳过。

Google Maps 可以轻松返回 10,000 家“伊利诺伊州的披萨餐厅”,但其中大多数可能不符合你的实际 ICP(例如,你可能只需要有 50-200 个座位的商家,或只需要没有在线订餐的商家)。在付费进行数据补充之前,先抽取约 50 条结果样本并运行 /icp-prompt-builder:

  1. 使用 AI 资格筛选提示词评估 10 条结果
  2. 标记“这个应该判为否,他们是连锁加盟店”
  3. 优化提示词,再运行下一批 10 条
  4. 连续 2 轮没有需要修正的地方时停止
  5. 使用调整后的提示词筛选其余抓取结果

**为何这是必需步骤:**后续的所有者信息查找(通过 /blitz-list-builder)和邮箱瀑布式查找,每个联系人需花费 $0.10-$0.30。抓取 10,000 家商户就要花费 $1K-$3K。提前进行资格筛选,平均可节省其中 50-80% 的支出。

开始前的准备

  1. 安装 Node.js 18+ 和 npm
  2. 注册 RapidAPI 帐户(提供免费套餐),并订阅 Maps Data API:

就是这些。无需 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 req/sec)和使用指数退避进行重试。

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 之外同时使用浏览器界面,请添加 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
  1. 将项目推送到 GitHub 仓库
  2. 前往 https://railway.com,创建新项目并连接仓库
  3. 在 Railway 控制面板中设置环境变量 RAPIDAPI_KEY
  4. 将启动命令设置为 npx tsx src/server.ts
  5. 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获取一家商家的完整详情?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(第 5 阶段,电子邮件)补齐缺失的电子邮件,然后运行 /list-quality-scorecard 评分。

**或者先等等:**如果抓取结果少于 200 家商家,说明你的查询词和位置范围太窄。继续之前先扩大范围。

相关技能

  • /icp-prompt-builder — 必需的资格筛选步骤
  • /blitz-list-builder — 查找各家商家的业主联系人
  • /list-builder(第 5 阶段,电子邮件)— 补齐缺失的电子邮件
  • /list-quality-scorecard — 为最终名单评分

评论

支持 Markdown