mcp-scraper 0.26.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/bin/api-server.cjs +309 -270
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +3 -2
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +3 -3
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +61 -27
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +64 -5
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs +2 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-FUWZWKGO.js → chunk-KAMKTSJQ.js} +2 -2
- package/dist/chunk-O6KFDDRM.js +7 -0
- package/dist/chunk-O6KFDDRM.js.map +1 -0
- package/dist/{chunk-QO2TRJ3L.js → chunk-RPR5LDLS.js} +24 -48
- package/dist/chunk-RPR5LDLS.js.map +1 -0
- package/dist/{chunk-E5J4HJBO.js → chunk-YH5DRFWQ.js} +4 -2
- package/dist/{chunk-XGIPATLV.js → chunk-ZQSHKWV4.js} +3 -2
- package/dist/chunk-ZQSHKWV4.js.map +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-K6ODZOZA.js → server-YXVSZ3T7.js} +323 -262
- package/dist/server-YXVSZ3T7.js.map +1 -0
- package/dist/{worker-E3KHQH2F.js → worker-RYP5EOV5.js} +3 -3
- package/docs/mcp-tool-manifest.generated.json +37 -8
- package/package.json +1 -1
- package/dist/chunk-QO2TRJ3L.js.map +0 -1
- package/dist/chunk-V37MZON3.js +0 -7
- package/dist/chunk-V37MZON3.js.map +0 -1
- package/dist/chunk-XGIPATLV.js.map +0 -1
- package/dist/server-K6ODZOZA.js.map +0 -1
- /package/dist/{chunk-FUWZWKGO.js.map → chunk-KAMKTSJQ.js.map} +0 -0
- /package/dist/{chunk-E5J4HJBO.js.map → chunk-YH5DRFWQ.js.map} +0 -0
- /package/dist/{worker-E3KHQH2F.js.map → worker-RYP5EOV5.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -17330,7 +17330,7 @@ var init_schemas3 = __esm({
|
|
|
17330
17330
|
"use strict";
|
|
17331
17331
|
import_zod17 = require("zod");
|
|
17332
17332
|
DEFAULT_PROXY_MODE = "none";
|
|
17333
|
-
DEFAULT_MAPS_PROXY_MODE = "
|
|
17333
|
+
DEFAULT_MAPS_PROXY_MODE = "none";
|
|
17334
17334
|
HarvestOptionsSchema = import_zod17.z.object({
|
|
17335
17335
|
query: import_zod17.z.string().min(1),
|
|
17336
17336
|
location: import_zod17.z.string().optional(),
|
|
@@ -17372,6 +17372,7 @@ var init_schemas3 = __esm({
|
|
|
17372
17372
|
gl: import_zod17.z.string().length(2).default("us"),
|
|
17373
17373
|
hl: import_zod17.z.string().length(2).default("en"),
|
|
17374
17374
|
maxResults: import_zod17.z.number().int().min(1).max(50).default(10),
|
|
17375
|
+
includeServices: import_zod17.z.boolean().default(false),
|
|
17375
17376
|
proxyMode: import_zod17.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
|
|
17376
17377
|
proxyZip: import_zod17.z.string().regex(/^\d{5}$/).optional(),
|
|
17377
17378
|
debug: import_zod17.z.boolean().default(false),
|
|
@@ -19227,12 +19228,277 @@ var init_video_routes = __esm({
|
|
|
19227
19228
|
}
|
|
19228
19229
|
});
|
|
19229
19230
|
|
|
19231
|
+
// src/extractor/MapsSearchExtractor.ts
|
|
19232
|
+
function buildGoogleLocalResultsUrl(input) {
|
|
19233
|
+
const query = [input.query, input.location].filter(Boolean).join(" ");
|
|
19234
|
+
const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
|
|
19235
|
+
if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
|
|
19236
|
+
return `https://www.google.com/search?${params.toString()}`;
|
|
19237
|
+
}
|
|
19238
|
+
var LOCAL_RESULTS_PAGE_LIMIT, LOCAL_RESULTS_WAIT_MS, MapsSearchExtractor;
|
|
19239
|
+
var init_MapsSearchExtractor = __esm({
|
|
19240
|
+
"src/extractor/MapsSearchExtractor.ts"() {
|
|
19241
|
+
"use strict";
|
|
19242
|
+
init_errors();
|
|
19243
|
+
init_uule();
|
|
19244
|
+
LOCAL_RESULTS_PAGE_LIMIT = 8;
|
|
19245
|
+
LOCAL_RESULTS_WAIT_MS = 1e3;
|
|
19246
|
+
MapsSearchExtractor = class {
|
|
19247
|
+
constructor(driver) {
|
|
19248
|
+
this.driver = driver;
|
|
19249
|
+
}
|
|
19250
|
+
driver;
|
|
19251
|
+
async extract(options) {
|
|
19252
|
+
const startMs = Date.now();
|
|
19253
|
+
const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
|
|
19254
|
+
const searchUrl = buildGoogleLocalResultsUrl(options);
|
|
19255
|
+
const config = {
|
|
19256
|
+
headless: options.headless,
|
|
19257
|
+
kernelApiKey: options.kernelApiKey,
|
|
19258
|
+
kernelProxyId: options.kernelProxyId,
|
|
19259
|
+
kernelProxyResolution: options.kernelProxyResolution,
|
|
19260
|
+
proxyMode: options.proxyMode,
|
|
19261
|
+
viewport: { width: 1280, height: 900 },
|
|
19262
|
+
locale: `${options.hl}-${options.gl.toUpperCase()}`,
|
|
19263
|
+
debug: options.debug
|
|
19264
|
+
};
|
|
19265
|
+
try {
|
|
19266
|
+
await this.driver.launch(config);
|
|
19267
|
+
const page = this.driver.getPage();
|
|
19268
|
+
await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
19269
|
+
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
19270
|
+
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19271
|
+
const results = await this.collectResults(page, options);
|
|
19272
|
+
return {
|
|
19273
|
+
query: options.query,
|
|
19274
|
+
location: options.location ?? null,
|
|
19275
|
+
searchQuery,
|
|
19276
|
+
searchUrl,
|
|
19277
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19278
|
+
requestedMaxResults: options.maxResults,
|
|
19279
|
+
resultCount: results.length,
|
|
19280
|
+
results,
|
|
19281
|
+
durationMs: Date.now() - startMs
|
|
19282
|
+
};
|
|
19283
|
+
} finally {
|
|
19284
|
+
await this.driver.close();
|
|
19285
|
+
}
|
|
19286
|
+
}
|
|
19287
|
+
async detectBlock(page) {
|
|
19288
|
+
return page.evaluate(() => {
|
|
19289
|
+
const text2 = document.body.innerText.slice(0, 2e3);
|
|
19290
|
+
return /unusual traffic|captcha|recaptcha|about this page/i.test(text2) || /\/sorry\//.test(location.href);
|
|
19291
|
+
});
|
|
19292
|
+
}
|
|
19293
|
+
async collectResults(page, options) {
|
|
19294
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19295
|
+
const results = [];
|
|
19296
|
+
let pageCount = 0;
|
|
19297
|
+
while (results.length < options.maxResults && pageCount < LOCAL_RESULTS_PAGE_LIMIT) {
|
|
19298
|
+
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19299
|
+
const cards = await this.extractVisibleCards(page, options.location);
|
|
19300
|
+
for (const card of cards) {
|
|
19301
|
+
if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
|
|
19302
|
+
seen.add(card.cardKey);
|
|
19303
|
+
const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
|
|
19304
|
+
results.push({
|
|
19305
|
+
position: results.length + 1,
|
|
19306
|
+
name: card.name,
|
|
19307
|
+
placeUrl: details?.placeUrl ?? card.placeUrl,
|
|
19308
|
+
cid: details?.cid ?? card.cid,
|
|
19309
|
+
cidDecimal: details?.cidDecimal ?? card.cidDecimal,
|
|
19310
|
+
rating: details?.rating ?? card.rating,
|
|
19311
|
+
reviewCount: details?.reviewCount ?? card.reviewCount,
|
|
19312
|
+
category: details?.category ?? card.category,
|
|
19313
|
+
address: details?.address ?? card.address,
|
|
19314
|
+
phone: details?.phone ?? card.phone,
|
|
19315
|
+
hoursStatus: details?.hoursStatus ?? card.hoursStatus,
|
|
19316
|
+
websiteUrl: details?.websiteUrl ?? card.websiteUrl,
|
|
19317
|
+
directionsUrl: card.directionsUrl,
|
|
19318
|
+
metadata: card.metadata,
|
|
19319
|
+
services: details?.services ?? [],
|
|
19320
|
+
areasServed: details?.areasServed ?? [],
|
|
19321
|
+
profileDetailsStatus: details ? "collected" : "unavailable"
|
|
19322
|
+
});
|
|
19323
|
+
}
|
|
19324
|
+
if (results.length >= options.maxResults) break;
|
|
19325
|
+
const advanced = await this.advanceToMoreResults(page);
|
|
19326
|
+
if (!advanced) break;
|
|
19327
|
+
pageCount += 1;
|
|
19328
|
+
}
|
|
19329
|
+
return results;
|
|
19330
|
+
}
|
|
19331
|
+
async extractVisibleCards(page, location2) {
|
|
19332
|
+
const fallbackLocation = location2 ?? "";
|
|
19333
|
+
return page.evaluate((locationHint) => {
|
|
19334
|
+
const normalize4 = (value) => {
|
|
19335
|
+
const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
19336
|
+
return text2 || null;
|
|
19337
|
+
};
|
|
19338
|
+
const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
|
|
19339
|
+
const addressPattern = /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i;
|
|
19340
|
+
const cards = Array.from(document.querySelectorAll("button")).filter((button) => {
|
|
19341
|
+
const name = normalize4(button.querySelector("h3")?.textContent);
|
|
19342
|
+
return Boolean(name) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText);
|
|
19343
|
+
});
|
|
19344
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19345
|
+
const out = [];
|
|
19346
|
+
for (const [cardIndex, card] of cards.entries()) {
|
|
19347
|
+
const name = normalize4(card.querySelector("h3")?.textContent);
|
|
19348
|
+
if (!name) continue;
|
|
19349
|
+
const text2 = normalize4(card.innerText) ?? name;
|
|
19350
|
+
const key = `${name.toLowerCase()}|${text2.toLowerCase()}`;
|
|
19351
|
+
if (seen.has(key)) continue;
|
|
19352
|
+
seen.add(key);
|
|
19353
|
+
const lines = text2.split(/\n+/).map((line) => normalize4(line)).filter((line) => Boolean(line));
|
|
19354
|
+
const aria = normalize4(card.getAttribute("aria-label"));
|
|
19355
|
+
const rating = (aria ?? text2).match(/(?:rated\s+)?(\d(?:\.\d)?)\s*(?:out of 5)?/i)?.[1] ?? null;
|
|
19356
|
+
const reviewCount = (aria ?? text2).match(/(?:\(|·\s*)([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
|
|
19357
|
+
const phone = lines.find((line) => phonePattern.test(line)) ?? null;
|
|
19358
|
+
const address = lines.find((line) => addressPattern.test(line)) ?? null;
|
|
19359
|
+
const hoursStatus = lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? null;
|
|
19360
|
+
const category = lines.find(
|
|
19361
|
+
(line) => line !== name && line !== phone && line !== address && line !== hoursStatus && !/^(?:rated|\d[\d.,]*\s*(?:out of 5|reviews?)?)\b/i.test(line) && !/sponsored|website|directions|call|save|share|more/i.test(line)
|
|
19362
|
+
) ?? null;
|
|
19363
|
+
const mapsSearchUrl = `https://www.google.com/maps/search/${encodeURIComponent([name, locationHint].filter(Boolean).join(" "))}`;
|
|
19364
|
+
out.push({
|
|
19365
|
+
cardIndex,
|
|
19366
|
+
cardKey: key,
|
|
19367
|
+
name,
|
|
19368
|
+
placeUrl: mapsSearchUrl,
|
|
19369
|
+
cid: null,
|
|
19370
|
+
cidDecimal: null,
|
|
19371
|
+
rating,
|
|
19372
|
+
reviewCount,
|
|
19373
|
+
category,
|
|
19374
|
+
address,
|
|
19375
|
+
phone,
|
|
19376
|
+
hoursStatus,
|
|
19377
|
+
websiteUrl: null,
|
|
19378
|
+
directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
|
|
19379
|
+
metadata: lines.slice(0, 20)
|
|
19380
|
+
});
|
|
19381
|
+
}
|
|
19382
|
+
return out;
|
|
19383
|
+
}, fallbackLocation);
|
|
19384
|
+
}
|
|
19385
|
+
async openCardAndExtractDialog(page, card, includeServices) {
|
|
19386
|
+
const clicked = await page.evaluate((input) => {
|
|
19387
|
+
const normalize4 = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
19388
|
+
const cards = Array.from(document.querySelectorAll("button")).filter(
|
|
19389
|
+
(button) => Boolean(button.querySelector("h3")) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText)
|
|
19390
|
+
);
|
|
19391
|
+
const exact = cards.find((button) => `${normalize4(button.querySelector("h3")?.textContent)}|${normalize4(button.innerText)}` === input.cardKey);
|
|
19392
|
+
const target = exact ?? cards[input.cardIndex];
|
|
19393
|
+
if (!target) return false;
|
|
19394
|
+
target.click();
|
|
19395
|
+
return true;
|
|
19396
|
+
}, { cardIndex: card.cardIndex, cardKey: card.cardKey });
|
|
19397
|
+
if (!clicked) return null;
|
|
19398
|
+
const dialog = page.locator('dialog, [role="dialog"]').filter({ has: page.locator('h1, h2, [role="heading"]') }).last();
|
|
19399
|
+
await dialog.waitFor({ state: "visible", timeout: 6e3 }).catch(() => {
|
|
19400
|
+
});
|
|
19401
|
+
if (await dialog.count() === 0 || !await dialog.isVisible().catch(() => false)) return null;
|
|
19402
|
+
const details = await dialog.evaluate((root, includeServicesValue) => {
|
|
19403
|
+
const text2 = (value) => value?.replace(/\s+/g, " ").trim() ?? "";
|
|
19404
|
+
const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
|
|
19405
|
+
const buttons = Array.from(root.querySelectorAll("button"));
|
|
19406
|
+
const buttonTexts = buttons.map((button) => text2(button.innerText)).filter(Boolean);
|
|
19407
|
+
const servicesText = includeServicesValue ? buttonTexts.find((value) => /^services\s*:/i.test(value)) : null;
|
|
19408
|
+
const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
|
|
19409
|
+
const areaText = includeServicesValue ? buttonTexts.find((value) => /^serves\b/i.test(value)) : null;
|
|
19410
|
+
const areasServed = areaText ? [areaText.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
|
|
19411
|
+
const externalLinks = Array.from(root.querySelectorAll("a[href]"));
|
|
19412
|
+
const website = externalLinks.find((link) => /^website$/i.test(text2(link.innerText)) || /^website$/i.test(link.getAttribute("aria-label") ?? ""));
|
|
19413
|
+
const mapLink = externalLinks.find((link) => /google\.[^/]+\/maps|\/maps\//i.test(link.href));
|
|
19414
|
+
const addressLink = externalLinks.find((link) => /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i.test(text2(link.innerText)));
|
|
19415
|
+
const allText = text2(root.innerText);
|
|
19416
|
+
const rating = allText.match(/\b(?:rated\s+)?(\d(?:\.\d)?)\s+out of 5\b/i)?.[1] ?? allText.match(/\b(\d(?:\.\d)?)\s+rated\b/i)?.[1] ?? null;
|
|
19417
|
+
const reviewCount = allText.match(/([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
|
|
19418
|
+
const phoneButton = buttonTexts.find((value) => /^call\b/i.test(value) || phonePattern.test(value)) ?? "";
|
|
19419
|
+
const phone = phoneButton.match(phonePattern)?.[0] ?? null;
|
|
19420
|
+
const hoursStatus = buttonTexts.find((value) => /\b(?:open|closed|opens|closes)\b/i.test(value)) ?? null;
|
|
19421
|
+
const placeUrl = mapLink?.href ?? addressLink?.href ?? location.href;
|
|
19422
|
+
const fid = placeUrl.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
|
|
19423
|
+
const cid = fid ? `${fid[1]}:${fid[2]}` : null;
|
|
19424
|
+
let cidDecimal = null;
|
|
19425
|
+
if (fid) {
|
|
19426
|
+
try {
|
|
19427
|
+
cidDecimal = BigInt(fid[2]).toString();
|
|
19428
|
+
} catch {
|
|
19429
|
+
}
|
|
19430
|
+
}
|
|
19431
|
+
if (!cidDecimal) {
|
|
19432
|
+
const cidFromQuery = new URL(placeUrl).searchParams.get("cid");
|
|
19433
|
+
cidDecimal = cidFromQuery && /^\d+$/.test(cidFromQuery) ? cidFromQuery : null;
|
|
19434
|
+
}
|
|
19435
|
+
return {
|
|
19436
|
+
placeUrl,
|
|
19437
|
+
cid,
|
|
19438
|
+
cidDecimal,
|
|
19439
|
+
rating,
|
|
19440
|
+
reviewCount,
|
|
19441
|
+
category: null,
|
|
19442
|
+
address: text2(addressLink?.innerText) || null,
|
|
19443
|
+
phone,
|
|
19444
|
+
websiteUrl: website?.href ?? null,
|
|
19445
|
+
hoursStatus,
|
|
19446
|
+
services,
|
|
19447
|
+
areasServed
|
|
19448
|
+
};
|
|
19449
|
+
}, includeServices);
|
|
19450
|
+
await this.closeDialog(page);
|
|
19451
|
+
return details;
|
|
19452
|
+
}
|
|
19453
|
+
async closeDialog(page) {
|
|
19454
|
+
const closed = await page.evaluate(() => {
|
|
19455
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]'));
|
|
19456
|
+
const dialog = dialogs.find((candidate) => candidate.offsetParent !== null);
|
|
19457
|
+
const close = dialog && Array.from(dialog.querySelectorAll("button")).find(
|
|
19458
|
+
(button) => /^close$/i.test(button.getAttribute("aria-label") ?? "") || /^close$/i.test(button.innerText.trim())
|
|
19459
|
+
);
|
|
19460
|
+
close?.click();
|
|
19461
|
+
return Boolean(close);
|
|
19462
|
+
});
|
|
19463
|
+
if (closed) await page.waitForTimeout(250);
|
|
19464
|
+
}
|
|
19465
|
+
async advanceToMoreResults(page) {
|
|
19466
|
+
const before = await page.evaluate(() => document.body.innerText.slice(-2e3));
|
|
19467
|
+
const advanced = await page.evaluate(() => {
|
|
19468
|
+
const visible = (element) => element.offsetParent !== null;
|
|
19469
|
+
const mobile = Array.from(document.querySelectorAll("button, a")).find(
|
|
19470
|
+
(element) => visible(element) && /^(more search results|more results)$/i.test(element.innerText.trim())
|
|
19471
|
+
);
|
|
19472
|
+
if (mobile) {
|
|
19473
|
+
mobile.click();
|
|
19474
|
+
return true;
|
|
19475
|
+
}
|
|
19476
|
+
const next = Array.from(document.querySelectorAll("a[href]")).find(
|
|
19477
|
+
(link) => visible(link) && (/^next$/i.test(link.innerText.trim()) || /[?&]start=\d+/.test(link.href))
|
|
19478
|
+
);
|
|
19479
|
+
if (next) {
|
|
19480
|
+
next.click();
|
|
19481
|
+
return true;
|
|
19482
|
+
}
|
|
19483
|
+
return false;
|
|
19484
|
+
});
|
|
19485
|
+
if (!advanced) return false;
|
|
19486
|
+
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
19487
|
+
await page.waitForFunction((previous) => document.body.innerText.slice(-2e3) !== previous, before, { timeout: 5e3 }).catch(() => {
|
|
19488
|
+
});
|
|
19489
|
+
return true;
|
|
19490
|
+
}
|
|
19491
|
+
};
|
|
19492
|
+
}
|
|
19493
|
+
});
|
|
19494
|
+
|
|
19230
19495
|
// src/extractor/MapsNavigator.ts
|
|
19231
19496
|
var MapsNavigator;
|
|
19232
19497
|
var init_MapsNavigator = __esm({
|
|
19233
19498
|
"src/extractor/MapsNavigator.ts"() {
|
|
19234
19499
|
"use strict";
|
|
19235
19500
|
init_selectors();
|
|
19501
|
+
init_MapsSearchExtractor();
|
|
19236
19502
|
MapsNavigator = class {
|
|
19237
19503
|
constructor(page) {
|
|
19238
19504
|
this.page = page;
|
|
@@ -19290,32 +19556,26 @@ var init_MapsNavigator = __esm({
|
|
|
19290
19556
|
await this.page.waitForSelector('[role="main"] h2', { timeout: 5e3 }).catch(() => {
|
|
19291
19557
|
});
|
|
19292
19558
|
}
|
|
19293
|
-
async navigateToSerpBusinessPanel(businessName, location2) {
|
|
19294
|
-
const
|
|
19295
|
-
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=1`;
|
|
19559
|
+
async navigateToSerpBusinessPanel(businessName, location2, gl, hl) {
|
|
19560
|
+
const searchUrl = buildGoogleLocalResultsUrl({ query: businessName, location: location2, gl, hl });
|
|
19296
19561
|
await this.page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
|
|
19297
19562
|
});
|
|
19298
|
-
await this.page.waitForTimeout(
|
|
19563
|
+
await this.page.waitForTimeout(1e3);
|
|
19299
19564
|
const clicked = await this.page.evaluate((name) => {
|
|
19300
|
-
const
|
|
19301
|
-
const
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
if (clickable) {
|
|
19307
|
-
clickable.click();
|
|
19308
|
-
return true;
|
|
19309
|
-
}
|
|
19310
|
-
if (heading) {
|
|
19311
|
-
heading.click();
|
|
19565
|
+
const normalized = name.replace(/\s+/g, " ").trim().toLowerCase();
|
|
19566
|
+
const card = Array.from(document.querySelectorAll("button")).find(
|
|
19567
|
+
(button) => button.querySelector("h3")?.textContent?.replace(/\s+/g, " ").trim().toLowerCase() === normalized
|
|
19568
|
+
);
|
|
19569
|
+
if (card) {
|
|
19570
|
+
card.click();
|
|
19312
19571
|
return true;
|
|
19313
19572
|
}
|
|
19314
19573
|
return false;
|
|
19315
19574
|
}, businessName);
|
|
19316
19575
|
if (!clicked) return false;
|
|
19317
|
-
await this.page.
|
|
19318
|
-
|
|
19576
|
+
await this.page.waitForSelector('dialog, [role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
19577
|
+
});
|
|
19578
|
+
return this.page.locator('dialog, [role="dialog"]').count().then((count) => count > 0);
|
|
19319
19579
|
}
|
|
19320
19580
|
buildReviewsUrl(placeUrl) {
|
|
19321
19581
|
const fid = placeUrl.match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i);
|
|
@@ -19508,7 +19768,7 @@ var init_MapsExtractor = __esm({
|
|
|
19508
19768
|
let servicesStatus = "not_requested";
|
|
19509
19769
|
if (options.includeServices) {
|
|
19510
19770
|
try {
|
|
19511
|
-
const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location);
|
|
19771
|
+
const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location, options.gl, options.hl);
|
|
19512
19772
|
if (opened) {
|
|
19513
19773
|
const result = await this.extractServicesAndAreasServed(page);
|
|
19514
19774
|
services = result.services;
|
|
@@ -19729,249 +19989,15 @@ var init_MapsExtractor = __esm({
|
|
|
19729
19989
|
return result.data;
|
|
19730
19990
|
}
|
|
19731
19991
|
async extractServicesAndAreasServed(page) {
|
|
19732
|
-
await page.waitForTimeout(1500);
|
|
19733
|
-
let services = [];
|
|
19734
|
-
if (await this.clickRowStartingWithRetry(page, "Services:")) {
|
|
19735
|
-
await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
19736
|
-
});
|
|
19737
|
-
await page.waitForTimeout(800);
|
|
19738
|
-
services = await this.extractOpenDialogItems(page, "Services");
|
|
19739
|
-
await this.closeOpenDialog(page, "Services");
|
|
19740
|
-
await page.waitForTimeout(500);
|
|
19741
|
-
}
|
|
19742
|
-
let areasServed = [];
|
|
19743
|
-
if (await this.clickRowStartingWithRetry(page, "Serves ")) {
|
|
19744
|
-
await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
19745
|
-
});
|
|
19746
|
-
await page.waitForTimeout(800);
|
|
19747
|
-
areasServed = await this.extractOpenDialogItems(page, "Areas Served");
|
|
19748
|
-
await this.closeOpenDialog(page, "Areas Served");
|
|
19749
|
-
}
|
|
19750
|
-
return { services, areasServed };
|
|
19751
|
-
}
|
|
19752
|
-
async clickRowStartingWithRetry(page, prefix) {
|
|
19753
|
-
if (await this.clickRowStartingWith(page, prefix)) return true;
|
|
19754
|
-
await page.waitForTimeout(2e3);
|
|
19755
|
-
return this.clickRowStartingWith(page, prefix);
|
|
19756
|
-
}
|
|
19757
|
-
async clickRowStartingWith(page, prefix) {
|
|
19758
|
-
return page.evaluate((p) => {
|
|
19759
|
-
const el2 = Array.from(document.querySelectorAll('button, a, div[role="button"]')).find(
|
|
19760
|
-
(e) => (e.innerText ?? "").trim().startsWith(p)
|
|
19761
|
-
);
|
|
19762
|
-
if (!el2) return false;
|
|
19763
|
-
el2.click();
|
|
19764
|
-
return true;
|
|
19765
|
-
}, prefix);
|
|
19766
|
-
}
|
|
19767
|
-
async extractOpenDialogItems(page, expectedHeading) {
|
|
19768
|
-
return page.evaluate((expected) => {
|
|
19769
|
-
const dialogs = Array.from(document.querySelectorAll('[role="dialog"]'));
|
|
19770
|
-
const withHeading = dialogs.find((d) => {
|
|
19771
|
-
const h = d.querySelector('h1, h2, [role="heading"]');
|
|
19772
|
-
const headingText = h?.innerText?.trim() ?? "";
|
|
19773
|
-
return headingText.startsWith(expected);
|
|
19774
|
-
});
|
|
19775
|
-
if (!withHeading) return [];
|
|
19776
|
-
const headingEl = withHeading.querySelector('h1, h2, [role="heading"]');
|
|
19777
|
-
const headingLines = new Set(
|
|
19778
|
-
(headingEl?.innerText ?? "").split("\n").map((s) => s.trim()).filter(Boolean)
|
|
19779
|
-
);
|
|
19780
|
-
const seen = /* @__PURE__ */ new Set();
|
|
19781
|
-
const items = [];
|
|
19782
|
-
withHeading.querySelectorAll("*").forEach((el2) => {
|
|
19783
|
-
if (el2.children.length > 0) return;
|
|
19784
|
-
const text2 = el2.innerText?.trim() ?? el2.textContent?.trim() ?? "";
|
|
19785
|
-
if (!text2 || headingLines.has(text2)) return;
|
|
19786
|
-
if (/^(back|close|done|thanks)$/i.test(text2)) return;
|
|
19787
|
-
if (/^serves\s.*following/i.test(text2)) return;
|
|
19788
|
-
if (seen.has(text2)) return;
|
|
19789
|
-
seen.add(text2);
|
|
19790
|
-
items.push(text2);
|
|
19791
|
-
});
|
|
19792
|
-
return items;
|
|
19793
|
-
}, expectedHeading);
|
|
19794
|
-
}
|
|
19795
|
-
async closeOpenDialog(page, expectedHeading) {
|
|
19796
|
-
await page.evaluate((expected) => {
|
|
19797
|
-
const dialog = Array.from(document.querySelectorAll('[role="dialog"]')).find((d) => {
|
|
19798
|
-
const h = d.querySelector('h1, h2, [role="heading"]');
|
|
19799
|
-
const headingText = h?.innerText?.trim() ?? "";
|
|
19800
|
-
return headingText.startsWith(expected);
|
|
19801
|
-
});
|
|
19802
|
-
const closeBtn = dialog?.querySelector('[aria-label*="Close" i], button[aria-label*="close" i]');
|
|
19803
|
-
closeBtn?.click();
|
|
19804
|
-
}, expectedHeading);
|
|
19805
|
-
}
|
|
19806
|
-
};
|
|
19807
|
-
}
|
|
19808
|
-
});
|
|
19809
|
-
|
|
19810
|
-
// src/extractor/MapsSearchExtractor.ts
|
|
19811
|
-
var MAPS_SEARCH_SCROLL_BUDGET_MS, MAPS_SEARCH_SCROLL_STEP_MS, MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS, MapsSearchExtractor;
|
|
19812
|
-
var init_MapsSearchExtractor = __esm({
|
|
19813
|
-
"src/extractor/MapsSearchExtractor.ts"() {
|
|
19814
|
-
"use strict";
|
|
19815
|
-
init_errors();
|
|
19816
|
-
MAPS_SEARCH_SCROLL_BUDGET_MS = 6e4;
|
|
19817
|
-
MAPS_SEARCH_SCROLL_STEP_MS = 1200;
|
|
19818
|
-
MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS = 4;
|
|
19819
|
-
MapsSearchExtractor = class {
|
|
19820
|
-
constructor(driver) {
|
|
19821
|
-
this.driver = driver;
|
|
19822
|
-
}
|
|
19823
|
-
driver;
|
|
19824
|
-
async extract(options) {
|
|
19825
|
-
const startMs = Date.now();
|
|
19826
|
-
const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
|
|
19827
|
-
const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(searchQuery)}?hl=${encodeURIComponent(options.hl)}`;
|
|
19828
|
-
const config = {
|
|
19829
|
-
headless: options.headless,
|
|
19830
|
-
kernelApiKey: options.kernelApiKey,
|
|
19831
|
-
kernelProxyId: options.kernelProxyId,
|
|
19832
|
-
kernelProxyResolution: options.kernelProxyResolution,
|
|
19833
|
-
proxyMode: options.proxyMode,
|
|
19834
|
-
viewport: { width: 1280, height: 900 },
|
|
19835
|
-
locale: `${options.hl}-${options.gl.toUpperCase()}`,
|
|
19836
|
-
debug: options.debug
|
|
19837
|
-
};
|
|
19838
|
-
try {
|
|
19839
|
-
await this.driver.launch(config);
|
|
19840
|
-
const page = this.driver.getPage();
|
|
19841
|
-
await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
19842
|
-
await page.waitForTimeout(3e3);
|
|
19843
|
-
const blocked = await this.detectBlock(page);
|
|
19844
|
-
if (blocked) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
19845
|
-
const results = await this.collectResults(page, options.maxResults);
|
|
19846
|
-
return {
|
|
19847
|
-
query: options.query,
|
|
19848
|
-
location: options.location ?? null,
|
|
19849
|
-
searchQuery,
|
|
19850
|
-
searchUrl,
|
|
19851
|
-
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19852
|
-
requestedMaxResults: options.maxResults,
|
|
19853
|
-
resultCount: results.length,
|
|
19854
|
-
results,
|
|
19855
|
-
durationMs: Date.now() - startMs
|
|
19856
|
-
};
|
|
19857
|
-
} finally {
|
|
19858
|
-
await this.driver.close();
|
|
19859
|
-
}
|
|
19860
|
-
}
|
|
19861
|
-
async detectBlock(page) {
|
|
19862
19992
|
return page.evaluate(() => {
|
|
19863
|
-
const
|
|
19864
|
-
return
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
while (Date.now() - started < MAPS_SEARCH_SCROLL_BUDGET_MS) {
|
|
19872
|
-
const before = seen.size;
|
|
19873
|
-
const batch = await this.extractVisibleResults(page);
|
|
19874
|
-
for (const result of batch) {
|
|
19875
|
-
const key = this.resultKey(result);
|
|
19876
|
-
if (!seen.has(key)) seen.set(key, { ...result, position: seen.size + 1 });
|
|
19877
|
-
if (seen.size >= maxResults) break;
|
|
19878
|
-
}
|
|
19879
|
-
if (seen.size >= maxResults) break;
|
|
19880
|
-
if (seen.size === before) noGrowthRounds += 1;
|
|
19881
|
-
else noGrowthRounds = 0;
|
|
19882
|
-
if (noGrowthRounds >= MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS) break;
|
|
19883
|
-
await page.evaluate(() => {
|
|
19884
|
-
const feed = document.querySelector('[role="feed"]');
|
|
19885
|
-
if (feed) {
|
|
19886
|
-
feed.scrollTop = feed.scrollHeight;
|
|
19887
|
-
} else {
|
|
19888
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
19889
|
-
}
|
|
19890
|
-
});
|
|
19891
|
-
await page.waitForTimeout(MAPS_SEARCH_SCROLL_STEP_MS);
|
|
19892
|
-
}
|
|
19893
|
-
return [...seen.values()].slice(0, maxResults);
|
|
19894
|
-
}
|
|
19895
|
-
resultKey(result) {
|
|
19896
|
-
return result.cidDecimal ?? result.placeUrl.replace(/[?&].*$/, "") ?? result.name;
|
|
19897
|
-
}
|
|
19898
|
-
async extractVisibleResults(page) {
|
|
19899
|
-
return page.evaluate(() => {
|
|
19900
|
-
function normalizeText(value) {
|
|
19901
|
-
const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
19902
|
-
return text2 || null;
|
|
19903
|
-
}
|
|
19904
|
-
function cidFromUrl(url) {
|
|
19905
|
-
const fid = url.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
|
|
19906
|
-
if (!fid) return { cid: null, cidDecimal: null };
|
|
19907
|
-
let cidDecimal = null;
|
|
19908
|
-
try {
|
|
19909
|
-
cidDecimal = BigInt(fid[2]).toString();
|
|
19910
|
-
} catch {
|
|
19911
|
-
}
|
|
19912
|
-
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal };
|
|
19913
|
-
}
|
|
19914
|
-
function textParts(card) {
|
|
19915
|
-
if (!card) return [];
|
|
19916
|
-
const parts = [];
|
|
19917
|
-
card.querySelectorAll("div, span").forEach((el2) => {
|
|
19918
|
-
const text2 = Array.from(el2.childNodes).filter((node) => node.nodeType === 3).map((node) => node.textContent?.trim() ?? "").filter((text3) => text3.length > 1 && text3.length < 140).join(" ");
|
|
19919
|
-
if (text2 && !parts.includes(text2)) parts.push(text2);
|
|
19920
|
-
});
|
|
19921
|
-
return parts;
|
|
19922
|
-
}
|
|
19923
|
-
function firstMatching(parts, pattern) {
|
|
19924
|
-
const value = parts.find((part) => pattern.test(part));
|
|
19925
|
-
return value ?? null;
|
|
19926
|
-
}
|
|
19927
|
-
function normalizedSet(values) {
|
|
19928
|
-
return new Set(values.filter(Boolean).map((value) => value.toLowerCase()));
|
|
19929
|
-
}
|
|
19930
|
-
const out = [];
|
|
19931
|
-
const seen = /* @__PURE__ */ new Set();
|
|
19932
|
-
const anchors = Array.from(document.querySelectorAll('a[href*="/maps/place/"]'));
|
|
19933
|
-
for (const anchor of anchors) {
|
|
19934
|
-
const placeUrl = anchor.href;
|
|
19935
|
-
const stableUrl = placeUrl.replace(/[?&].*$/, "");
|
|
19936
|
-
if (seen.has(stableUrl)) continue;
|
|
19937
|
-
seen.add(stableUrl);
|
|
19938
|
-
const card = anchor.closest('.Nv2PK, [role="article"], .bfdHYd') ?? anchor.parentElement;
|
|
19939
|
-
const parts = textParts(card);
|
|
19940
|
-
const aria = normalizeText(anchor.getAttribute("aria-label"));
|
|
19941
|
-
const heading = normalizeText(card?.querySelector('.qBF1Pd, .fontHeadlineSmall, [role="heading"]')?.textContent);
|
|
19942
|
-
const name = aria ?? heading ?? parts[0] ?? stableUrl;
|
|
19943
|
-
const links = Array.from(card?.querySelectorAll("a[href]") ?? []);
|
|
19944
|
-
const websiteUrl = links.find((link) => link.href.startsWith("http") && !link.href.includes("google."))?.href ?? null;
|
|
19945
|
-
const rating = firstMatching(parts, /^\d(?:\.\d)?$/);
|
|
19946
|
-
const reviewCountRaw = firstMatching(parts, /^\(?[\d,]+\)?(?:\s+reviews?)?$/i);
|
|
19947
|
-
const phone = firstMatching(parts, /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/);
|
|
19948
|
-
const hoursStatus = parts.find((part) => /^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b/i.test(part)) ?? null;
|
|
19949
|
-
const address = parts.find((part) => /\b[A-Z]{2}\s+\d{5}\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Drive|Dr)\b/i.test(part)) ?? null;
|
|
19950
|
-
const directionsUrl = links.find((link) => /google\.[^/]+\/maps\/dir|\/dir\//i.test(link.href))?.href ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", ") || name)}`;
|
|
19951
|
-
const excluded = normalizedSet([name, rating, reviewCountRaw, phone, hoursStatus, address, "Website", "Directions"]);
|
|
19952
|
-
const category = parts.find((part) => {
|
|
19953
|
-
const normalized = part.toLowerCase();
|
|
19954
|
-
return !excluded.has(normalized) && !/^\d(?:\.\d)?$|^\(?[\d,]+\)?(?:\s+reviews?)?$/i.test(part) && !/(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/.test(part) && !/\b[A-Z]{2}\s+\d{5}\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Drive|Dr)\b/i.test(part) && !/^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b|directions|website|book online|sponsored|visit site|financing/i.test(part);
|
|
19955
|
-
}) ?? null;
|
|
19956
|
-
const { cid, cidDecimal } = cidFromUrl(placeUrl);
|
|
19957
|
-
out.push({
|
|
19958
|
-
position: out.length + 1,
|
|
19959
|
-
name,
|
|
19960
|
-
placeUrl,
|
|
19961
|
-
cid,
|
|
19962
|
-
cidDecimal,
|
|
19963
|
-
rating,
|
|
19964
|
-
reviewCount: reviewCountRaw ? reviewCountRaw.replace(/[()]/g, "") : null,
|
|
19965
|
-
category,
|
|
19966
|
-
address,
|
|
19967
|
-
phone,
|
|
19968
|
-
hoursStatus,
|
|
19969
|
-
websiteUrl,
|
|
19970
|
-
directionsUrl,
|
|
19971
|
-
metadata: parts.slice(0, 20)
|
|
19972
|
-
});
|
|
19973
|
-
}
|
|
19974
|
-
return out;
|
|
19993
|
+
const visible = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).find((dialog) => dialog.offsetParent !== null);
|
|
19994
|
+
if (!visible) return { services: [], areasServed: [] };
|
|
19995
|
+
const values = Array.from(visible.querySelectorAll("button")).map((button) => button.innerText.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
19996
|
+
const servicesText = values.find((value) => /^services\s*:/i.test(value));
|
|
19997
|
+
const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
|
|
19998
|
+
const served = values.find((value) => /^serves\b/i.test(value));
|
|
19999
|
+
const areasServed = served ? [served.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
|
|
20000
|
+
return { services, areasServed };
|
|
19975
20001
|
});
|
|
19976
20002
|
}
|
|
19977
20003
|
};
|
|
@@ -22721,7 +22747,10 @@ function formatMapsSearch(raw, input) {
|
|
|
22721
22747
|
const normalizedResults = results.map((result) => ({
|
|
22722
22748
|
...result,
|
|
22723
22749
|
phone: result.phone ?? null,
|
|
22724
|
-
hoursStatus: result.hoursStatus ?? null
|
|
22750
|
+
hoursStatus: result.hoursStatus ?? null,
|
|
22751
|
+
services: result.services ?? [],
|
|
22752
|
+
areasServed: result.areasServed ?? [],
|
|
22753
|
+
profileDetailsStatus: result.profileDetailsStatus ?? "unavailable"
|
|
22725
22754
|
}));
|
|
22726
22755
|
const searchQuery = d.searchQuery ?? [input.query, input.location].filter(Boolean).join(" ");
|
|
22727
22756
|
const requestedMax = d.requestedMaxResults ?? input.maxResults ?? 10;
|
|
@@ -22749,9 +22778,14 @@ ${meta}`;
|
|
|
22749
22778
|
|---|------|----------|--------|---------|-----|---------|------|
|
|
22750
22779
|
${rows}`,
|
|
22751
22780
|
metadataSection,
|
|
22781
|
+
results.some((result) => result.services?.length || result.areasServed?.length) ? `
|
|
22782
|
+
## Services and Areas Served
|
|
22783
|
+
${results.filter((result) => result.services?.length || result.areasServed?.length).map((result) => `### ${result.position}. ${result.name}
|
|
22784
|
+
${result.services?.length ? `- **Services:** ${result.services.join(", ")}` : ""}${result.areasServed?.length ? `
|
|
22785
|
+
- **Serves:** ${result.areasServed.join("; ")}` : ""}`).join("\n\n")}` : null,
|
|
22752
22786
|
`
|
|
22753
22787
|
---
|
|
22754
|
-
\u{1F4A1} **Next step:** use \`maps_place_intel\` with a selected business name and location
|
|
22788
|
+
\u{1F4A1} **Next step:** use \`maps_place_intel\` with a selected business name and location for full hours, review topics, and optional review cards. Set \`includeServices: true\` on this search to enrich the ranked list in one local-results session.`,
|
|
22755
22789
|
durationMs != null ? `
|
|
22756
22790
|
*Extracted in ${(durationMs / 1e3).toFixed(1)}s*` : null
|
|
22757
22791
|
].filter(Boolean).join("\n");
|
|
@@ -23802,6 +23836,7 @@ async function searchCity(options, market) {
|
|
|
23802
23836
|
gl: options.gl,
|
|
23803
23837
|
hl: options.hl,
|
|
23804
23838
|
maxResults: options.maxResultsPerCity,
|
|
23839
|
+
includeServices: false,
|
|
23805
23840
|
headless: options.headless,
|
|
23806
23841
|
kernelApiKey: options.kernelApiKey,
|
|
23807
23842
|
proxyMode: options.proxyMode,
|
|
@@ -29688,7 +29723,7 @@ var PACKAGE_VERSION;
|
|
|
29688
29723
|
var init_version = __esm({
|
|
29689
29724
|
"src/version.ts"() {
|
|
29690
29725
|
"use strict";
|
|
29691
|
-
PACKAGE_VERSION = "0.
|
|
29726
|
+
PACKAGE_VERSION = "0.27.0";
|
|
29692
29727
|
}
|
|
29693
29728
|
});
|
|
29694
29729
|
|
|
@@ -30279,8 +30314,9 @@ var init_mcp_tool_schemas = __esm({
|
|
|
30279
30314
|
gl: import_zod35.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
30280
30315
|
hl: import_zod35.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
30281
30316
|
maxResults: import_zod35.z.number().int().min(1).max(50).default(10).describe("Number of candidates to return. Default 10, maximum 50."),
|
|
30282
|
-
|
|
30283
|
-
|
|
30317
|
+
includeServices: import_zod35.z.boolean().default(false).describe("Open each returned Google Business Profile and include configured services and areas served when available. Does not collect review cards."),
|
|
30318
|
+
proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Leave unset for the default direct browser route. Google localization comes from the city in the query plus UULE, gl, and hl. location is an explicit residential-proxy override."),
|
|
30319
|
+
proxyZip: import_zod35.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override when proxyMode is location."),
|
|
30284
30320
|
debug: import_zod35.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
|
|
30285
30321
|
};
|
|
30286
30322
|
DirectoryWorkflowInputSchema = {
|
|
@@ -30294,7 +30330,7 @@ var init_mcp_tool_schemas = __esm({
|
|
|
30294
30330
|
includeZipGroups: import_zod35.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available (MCP_SCRAPER_USZIPS_CSV_PATH or usZipsCsvPath)."),
|
|
30295
30331
|
usZipsCsvPath: import_zod35.z.string().optional().describe("Local/test-only path to a US ZIPS CSV (state_abbr, zipcode, county, city columns). Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead. For ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the server, or pass this in local/test mode."),
|
|
30296
30332
|
saveCsv: import_zod35.z.boolean().default(true).describe("Save a directory-ready CSV of results to the MCP Scraper output directory and return its path."),
|
|
30297
|
-
proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy
|
|
30333
|
+
proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy behavior per city search. Leave unset for the direct localized Google route; location is an explicit residential-proxy override."),
|
|
30298
30334
|
proxyZip: import_zod35.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting; normally omitted."),
|
|
30299
30335
|
debug: import_zod35.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
|
|
30300
30336
|
};
|
|
@@ -30365,7 +30401,10 @@ var init_mcp_tool_schemas = __esm({
|
|
|
30365
30401
|
hoursStatus: NullableString,
|
|
30366
30402
|
websiteUrl: NullableString,
|
|
30367
30403
|
directionsUrl: NullableString,
|
|
30368
|
-
metadata: import_zod35.z.array(import_zod35.z.string())
|
|
30404
|
+
metadata: import_zod35.z.array(import_zod35.z.string()),
|
|
30405
|
+
services: import_zod35.z.array(import_zod35.z.string()).default([]),
|
|
30406
|
+
areasServed: import_zod35.z.array(import_zod35.z.string()).default([]),
|
|
30407
|
+
profileDetailsStatus: import_zod35.z.enum(["collected", "none_exist", "unavailable", "not_requested"]).default("unavailable")
|
|
30369
30408
|
})),
|
|
30370
30409
|
attempts: import_zod35.z.array(MapsSearchAttemptOutput),
|
|
30371
30410
|
durationMs: import_zod35.z.number().int().min(0)
|
|
@@ -32111,7 +32150,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
32111
32150
|
}, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
|
|
32112
32151
|
server.registerTool("maps_search", {
|
|
32113
32152
|
title: "Google Maps Business Search",
|
|
32114
|
-
description: "Search Google
|
|
32153
|
+
description: "Search Google local results for multiple businesses by category, niche, or local market \u2014 leads, prospects, competitors, or beyond the 3-pack. It paginates the rendered local-results list, opens each exact business card, then closes the profile dialog before continuing. Set includeServices for services and areas served; review cards are never collected by this tool.",
|
|
32115
32154
|
inputSchema: MapsSearchInputSchema,
|
|
32116
32155
|
outputSchema: recordOutputSchema("maps_search", MapsSearchOutputSchema),
|
|
32117
32156
|
annotations: liveWebToolAnnotations("Google Maps Business Search")
|