DEV Community

Techforce Global
Techforce Global

Posted on

From Google Maps to a Sales Pipeline: How I Realized Scraping Was the Easy Part

A build story for the Google Maps Business Leads Scraper & Sales Intelligence Actor

When I first started working on this Actor, the idea was actually pretty simple.

I wanted to build a Google Maps scraper that could help us find businesses and collect their basic information: business name, address, phone number, website, Google rating, reviews.

Nothing too complicated. And it worked.

But after looking at the output, I had the same thought I've had with a lot of scraping projects: okay, I have the data, now what?

A salesperson doesn't need another CSV with 500 business names. They need to know which businesses are worth contacting, why they should contact them, and ideally, they shouldn't have to spend three hours opening websites one by one to figure that out.

That was the point where this stopped being a Google Maps scraper for me. I wanted to turn it into a lead intelligence tool.

The problem: manual research is where the time disappears

The original problem was manual effort. A salesperson might start with something as simple as "search for resorts in Los Angeles." Then comes the actual work open Google Maps, check the business, open the website, find an email, check whether the website is actually good, look for SEO problems, look for technical problems, try to understand what service could be sold to that business, then decide whether the lead is worth contacting. Repeat for dozens or hundreds of businesses.

Scraping is the easy part. The manual research after scraping is where the time disappears. So I started asking whether the Actor could do more of that work automatically.

The first version was just a Google Maps scraper

I started with the basic version: search Google Maps → collect businesses → return structured data. That proved the idea worked, but there was nothing special about it. There are already loads of Google Maps scrapers on Apify I didn't want to build another one just because I could.

So instead of asking "how can I scrape more businesses," I asked: what information would actually help a salesperson make a decision? That changed the direction of the project.

From scraping to sales intelligence a real run, with real numbers

The Actor now starts with Google Maps, but it doesn't stop there. In one of my tests I searched "Resorts", location “Los Angeles”, capped at 10 businesses, with all four intelligence modules enabled: Sales & Growth Strategy, Website Performance Report, Suggested Business Improvements, and Website Technology Details

The Actor visits the Google Maps results, collects the business information, then visits the business website and starts analyzing it this is where Playwright matters, since the Actor needs to work with real rendered websites, not static HTML responses.

Here's a, run, with actual Console evidence rather than just a description:

Run: "Resorts" in "Los Angeles" - 10 results, 2m 28s, succeeded 2026-08-26


The actual configuration for this run:

run_input = {
    "searchQuery": "Resorts",
    "location": "Los Angeles",
    "subcategory": "",
    "maxResults": 10,
    "includeSalesStrategy": True,
    "includeServiceRecommendations": True,
    "includeTechnicalIntel": True,
    "includeWebsiteHealthScorecard": True,
    "deliveryMode": "summary",
    "mcpConnector": "<your Notion connector ID>",
    "mcpTool": "notion-create-pages",
    "mcpArguments": {
        "parent": {"page_id": "<your Notion page ID>"},
        "pages": [
            {
                "properties": {
                    "title": "Leads: {searchQuery} in {location} ({leadCount})"
                },
                "content": "{leads}"
            }
        ]
    },
    "proxyConfiguration": {"useApifyProxy": False}
}
Enter fullscreen mode Exit fullscreen mode

Run it with the Apify client:

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("techforce.global/google-maps-leads-sales-intelligence-tool").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["businessName"], item["googleRating"], item["totalReviews"])

Enter fullscreen mode Exit fullscreen mode

The output columns from that run: business name, company email, phone, address, Google rating, total reviews, category, website, social media, Google Maps URL, search query, and a Business Growth Opportunity block generated per lead.

The final result isn't just a business record. It's a combination of business data, website intelligence, and sales intelligence and that combination is the part I actually cared about.

What does the output look like?

Here's an actual result from the Four Seasons test run.

Business name: Four Seasons Hotel Los Angeles At Beverly Hills
Address: 300 S Doheny Dr, Los Angeles, CA 90048
Phone: (310) 273-2222
Google rating: 4.6
Total reviews: 2,411
Website: Available
Company email: No email found
That alone is useful. But the interesting part starts after that the Actor generated a lead overview:

Lead priority: Hot Lead
Revenue opportunity: High
Sales angle: Strong reputation but website trust and SEO polish gaps
Estimated monthly service potential: $300–$900
Estimated one-time project potential: $900–$2,400

Instead of saying "here's a hotel with a 4.6 rating," I can say: "this business already has strong social proof, but there are website trust, SEO, and conversion opportunities worth discussing." That's a much better starting point and it was probably the biggest shift in the whole project. I stopped thinking about the output as scraped data. I started thinking about it as a sales decision.

A note on differentiation: at the time of writing I don't yet have a saved example of a Low Priority lead to show side by side I've seen the Actor produce them, but I didn't keep one from an earlier run. What I can show instead is exactly how the tool tells the two apart, which is arguably more useful than one more example: see the scoring logic below.

How lead priority and recommendations actually get decided

I didn't want "Hot Lead" to be a field that just sounds confident without any reasoning behind it. Here's the actual shape of the logic, with exact thresholds abstracted since this is scoring logic I'd rather not hand verbatim to every other Google Maps scraper on the market:

recommendations: list[dict] = []
for field, service, required_work, expected_impact, why_this_matters, expected_after_update in service_rules:
    score = float(scores.get(field, 0) or 0)
    if score >= HIGH_QUALITY_THRESHOLD:
        continue  # already strong, no recommendation needed
    priority = "High" if score < LOW_QUALITY_THRESHOLD else "Medium"
    grade, classification = grade_from_score(score)
    recommendations.append({
        "service": service,
        "priority": priority,
        "currentGrade": grade,
        "professionalClassification": classification,
        "requiredWork": required_work,
        "expectedImpact": expected_impact,
        "whyThisChangeMatters": why_this_matters,
        "whatWillBeAchievedAfterUpdate": expected_after_update,
    })

if not recommendations:
    # Every scored category came back strong — recommend monitoring, not a rebuild
    recommendations.append({
        "service": "Website Monitoring and Continuous Optimization",
        "priority": "Low",
        "currentGrade": "A",
        "professionalClassification": "Growth-ready: maintain current quality through regular checks",
        "requiredWork": "Monitor speed, SEO, security headers, structured data, and conversion paths after every website update.",
        "expectedImpact": "Keeps the website stable, competitive, and ready for future growth.",
        "whyThisChangeMatters": "A strong website can still lose performance over time as plugins, content, tracking scripts, and search requirements change.",
        "whatWillBeAchievedAfterUpdate": "Regular monitoring should preserve website quality, keep growth campaigns stable, and help catch issues before they affect leads or revenue.",
    })

recommendations.sort(key=lambda item: {"High": 0, "Medium": 1, "Low": 2}.get(item["priority"], 3))
Enter fullscreen mode Exit fullscreen mode

In plain language: each website gets scored across several categories (SEO, security, performance, and a few others). Anything scoring above the high-quality threshold gets skipped no point recommending work on something that's already solid. Anything below the lower threshold becomes a High priority recommendation; the middle band becomes Medium. If a business scores well across the board, instead of returning nothing, the Actor recommends ongoing monitoring rather than forcing a fake "issue" just to have something to say. Recommendations are then sorted so the highest-priority, highest-impact items surface first.

That's also why a Hot Lead and a genuinely well-optimized business get treated differently under the hood, even if both show up with good Google ratings — the lead score and the website-quality score are measuring different things.

The website analysis: every site is different

Once the Actor had the business information, the next challenge was pulling something useful from the website itself and this wasn't as straightforward as I expected. Some sites load fast, some don't. Some have clean HTML, some rely heavily on JavaScript. Some expose an email, some don't. Some have good SEO, some look untouched since 2014 and some manage to have both problems and a great Google rating.

The Actor checks multiple parameters before generating its technical and sales analysis. For the Four Seasons test, the generated analysis flagged issues around on-page SEO, SSL/security, and technical SEO which gives a salesperson something concrete to open a conversation with:

Not: "Hi, we provide digital marketing services."

But something closer to: "We noticed that your business already has strong reviews and a good online foundation, but there are a few trust, SEO, and conversion improvements that could help improve enquiry confidence and organic visibility."

The first message sounds like an advertisement. The second one sounds like someone actually looked at the business. That difference was the entire goal.

The hardest problem wasn't scraping Google Maps

Ironically, the hardest part of this project wasn't Google Maps, and it wasn't even getting the website data. It was accuracy. If I'm going to give a salesperson a field called "Hot Lead," I need to be careful about what that actually means. If I say a business has a high revenue opportunity, there should be reasoning behind it, not a generic template with the business name inserted.

The way I actually tested this: run it, look at the result, find something that doesn't make sense, change the implementation, run it again, check the output again, repeat. One concrete example early on, businesses with genuinely excellent websites were still occasionally getting flagged with generic "improve your website" recommendations, because the scoring only looked at surface-level signals like page load time. Adding the category-by-category threshold logic above (rather than one blended score) is what fixed that a business can now score well on security but poorly on SEO, and get a recommendation that reflects the actual gap instead of an average that hides it.

There wasn't a magic switch that made results suddenly perfect. It was a lot of small improvements, and it's the least glamorous part of building a tool like this you don't see it on the Actor page, but it's where most of the work happens

The timeout problem

The basic Google Maps scraping was relatively fast. Once I added website analysis, technical analysis, and sales intelligence, every business became more expensive in execution time the Actor wasn't doing one operation anymore, it was doing several per business. If I scrape 100 businesses, I don't just have 100 Google Maps pages, I potentially have 100 websites that also need visiting and analyzing.

That's a genuinely different performance problem, and it meant keeping a close eye on execution time to keep the whole thing practical to run on the Apify platform which is also one of the reasons I like building on Apify: I don't have to build and manage the infrastructure around the crawler myself. I can focus on the Actor's actual logic.

What I actually used from Apify

  • Actor : the core execution unit
  • Playwright : handles browser-based website interaction, since static HTML fetching isn't enough for modern sites
  • Dataset : where structured lead results go
  • Key-value store : useful for data that doesn't fit a table-like dataset
  • Integrations : what turns the output into part of another workflow, not a dead end

Each run gets its own dataset and key-value store, which is convenient for development and testing I don't have to build a storage layer around every scraping project. I build the Actor, run it, and inspect results from the Console or the API.

The development cycle was short testing it properly wasn't

The first usable version took two to three days, but that doesn't mean two days of coding and done. I tested the Actor against hundreds of businesses during that process. The goal wasn't just making the Actor run successfully a run finishing with status "SUCCEEDED" is not the same thing as a run producing something anyone wants to use. That distinction mattered more than I expected going in.

The Apify Console made this testing loop much easier, since I could see input, logs, output, and storage for each run without building separate tooling. The runs used Apify SDK 3.4.1, Apify Client 2.5.1, and Crawlee 1.9.1.

From dataset to actual sales workflow

I didn't want the output to live forever inside an Apify dataset a sales team isn't going to open Apify every morning to check a dataset. They already have tools they use. For this workflow, results get pushed directly into Notion via Apify's MCP connector, using the notion-create-pages tool each run creates a page under a parent Notion page, titled with the search query, location, and lead count, with the leads themselves as page content.

So the pipeline is: Google Maps → the Lead Intelligence Actor → Apify Dataset → Notion → the sales workflow. The Actor isn't sitting at the end of the pipeline as a scraper it's one component in the middle of it. The same output could just as easily be pushed into a CRM, sent through an n8n workflow, or used to trigger another Actor entirely, since Apify supports Actor-to-Actor workflows where one Actor's output becomes the next step's input or trigger.

Why Apify?

I could have built the infrastructure myself managed browser instances, managed proxies, built storage, handled scaling and deployment and monitoring and eventually gotten back to the actual problem I wanted to solve. I didn't want to do that. Apify gives me a developer-friendly platform where I can focus on building the Actor instead of the infrastructure around it, and proxy support mattered too infrastructure problems can quickly become the project if you're not careful, and I wanted the project to stay the Actor.

What I would do differently next time

If I were starting this again, I'd talk to more salespeople before writing the first line of code not developers, not scraping experts, actual salespeople and agencies. What do you actually look at before contacting a lead? What makes you reject one? Which website problems are actually worth knowing? What makes a lead "hot" for you? What information helps you personalize a first message, and what's completely useless?

I built a lot of the intelligence based on what I thought would be useful. That works to a point, but there's a difference between "this is interesting information" and "this information actually changes whether I contact this lead." The second one is what I'd optimize for if I rebuilt this today I'd rather have ten highly useful fields than fifty nobody looks at.

Where I want to take this next

The current version can discover businesses, collect their information, visit their websites, analyze technical aspects, generate website health information, identify improvements, and generate sales-oriented intelligence. But I don't think the interesting part ends there.

Instead of just saying "Hot Lead," I want to surface why. Instead of just giving a sales angle, I want the salesperson to understand why that angle was selected. Instead of just generating a recommended pitch, I want the recommendation traceable back to actual signals from the business and its website not a black box.

The original idea was a Google Maps scraper. That version worked, but it wasn't enough. The interesting part started when I stopped asking "how many businesses can I scrape" and started asking "what can I tell a salesperson that will actually save them time." That question changed the entire project, and it's probably the biggest thing I learned building it.

Scraping the data is only half the job. Making the data useful is the real work. And sometimes the best scraper isn't the one that gives you the most data it's the one that makes you open the next lead and think: okay, this one is actually worth calling.

FAQ

Does this work for any business category, or just hotels and dental practices?
The Actor works with any Google Maps search query and location — the examples in this article (resorts, dentists) are just what I happened to test with. The intelligence modules apply the same scoring logic regardless of category

How is "Lead Priority" actually calculated?
Each business's website is scored across several categories (SEO, security, performance, and others). Scores below a threshold generate prioritized recommendations; scores above it don't. See the scoring logic section above for the actual (lightly abstracted) code.

What happens if a business has no email or no website at all?
The Actor still returns the Google Maps data (name, address, phone, rating, reviews) website-dependent fields like technical intel and website health scoring are simply left empty rather than guessed at.

Can I deliver results somewhere other than Notion?
Yes, Apify's MCP connector supports multiple delivery targets (Slack, Airtable, Google Sheets, and others depending on what's configured on your account). The dataset is always written in full regardless of delivery settings.

Try it yourself: Google Maps Business Leads Scraper & Sales Intelligence on Apify start with a small maxResults value (5–10) to see the output shape before scaling up a run.

Top comments (0)