AGM Studios Alexander G. Myers

Work · Marketing operations and SEO

How I measured search visibility when the search engines pushed back

I built a repeatable benchmark across three search engines. It turned a vague 'are we findable?' into a scored list of gaps and the page changes I recommended to close them.

Context A regional business-support program (unnamed): free advisory services, competing with larger agencies in search My role I designed the harness, ran it in seven iterations, and wrote the findings and recommendations. Tools Crawl4AI, dev-browser, Chrome DevTools Protocol, Python, Docker

Reconstruction. Fictional company and data, built from scratch to show a similar methodology. The original work is covered by a confidentiality agreement (NDA). The real benchmark was run during a 2026 internship at a regional business-advisory firm, listed on my CV.

The problem

The program offered free advice to small businesses across a large region, and nobody knew how often it showed up when an owner searched for help. Paid SEO suites were out of budget, so I answered the question directly. I wrote hundreds of queries for the region and its industries, ran them on Google, Bing and DuckDuckGo with a Python script I wrote around Crawl4AI, an open-source crawler, and logged every result. The tables and charts went to the team with the page changes I recommended.

The method

View diagram full sizeScroll sideways to see all of it
Query banktopic × region × phrasing,plus control queriesThree enginestwo read by a crawler,one by a browser sessionParseresults in page order,redirect links unwrappedScorerank → expected click share,mean per query setReportgaps, who owns them,what to changeblock page?stop and diagnoseReconstruction: similar methodology for a fictional regionalbusiness-support program.
Every iteration followed the same five steps, so results stayed comparable as the query bank grew.
made with draw.io last committed 2026-10-25 source file ↓
  1. I built the query bank deliberately. Each topic had several framings (the region’s name, a nearby town, “small business help”, a plain service phrase), so the result would show which framing won, not only which topic.
  2. I added controls. Every batch included raw service queries the program should not rank for. If those scored above zero, the measurement was leaking.
  3. I read three engines. Two through a self-hosted Crawl4AI instance driven by my query script, one through a real browser session.
  4. I scored by expected clicks, not by “ranked or not” (below).
  5. I reported gaps with their owners: which organisations held the spots the program was missing, and whether the gap was winnable or a different intent altogether.

When the engines push back

One engine served a block page as soon as the automated browser arrived. I assumed the IP address at first. It wasn’t. The tell was the browser announcing itself as automated. Running an ordinary browser session on the same connection, without the automation flag, returned normal results unattended, with no proxy and no CAPTCHA service.

Two smaller fixes mattered as much:

  • One engine wrapped every result link in a redirect. The first parser read those as “no results” and reported false absences until I decoded the links.
  • One engine rate-limited after about a dozen consecutive queries. I retried zero results after that point individually and slowly before they counted.
# Similar methodology, rewritten for this page:
# unwrap a redirect-style result link to its real destination.
from base64 import urlsafe_b64decode
from urllib.parse import urlparse, parse_qs, unquote

def real_url(href: str) -> str:
    q = parse_qs(urlparse(href).query)
    if "u" in q and q["u"][0].startswith("a1"):   # base64url after a prefix
        raw = q["u"][0][2:]
        return urlsafe_b64decode(raw + "=" * (-len(raw) % 4)).decode("utf-8", "replace")
    for key in ("uddg", "q", "url"):              # percent-encoded targets
        if key in q:
            return unquote(q[key][0])
    return href

The score

A first-place result gets far more clicks than a tenth-place one, so a simple “ranked or not” overstates weak placements. I weighted each rank by an expected click share, then took the mean for a query set.

Try the score

Set where the site ranks for five queries (0 = not on page one). The score is the mean expected click share.

0.000 visibility score · buckets:

What the synthetic run shows

00.10.20.3v1 region-framed 0.21v1 other framings 0.03v1v2 region-framed 0.19v2 other framings 0.02v2v3 region-framed 0.17v3 other framings 0.01v3v4 region-framed 0.22v4 other framings 0.02v4v5 region-framed 0.2v5 other framings 0v5v6 region-framed 0.24v6 other framings 0.01v6v7 region-framed 0.26v7 other framings 0.02v7region name in the querytown, province or generic
Visibility score per iteration
IterationRegion nameOther framings
v10.210.03
v20.190.02
v30.170.01
v40.220.02
v50.20
v60.240.01
v70.260.02
Score per iteration, split by how the query was framed. Synthetic numbers with the same shape as the real results, which stay under NDA.
made with Hugo template from a data file (SVG)data synthetic

The synthetic run shows the kind of pattern the method surfaces: a site that reaches page one only when a query carries one particular phrase, and is absent for the same topic phrased another way. A pattern like that turns a long list of gaps into one actionable change: lead titles and headings with the phrase people actually search.

What it proves

MeasurementA benchmark that can be rerun monthly and compared, with control queries that catch false positives.
DiagnosisI found the real cause of the blocking instead of paying for a workaround.
CostNo licence fees: self-hosted tools and a scoring method anyone can audit.

In an interview, I can walk through

  • how I mined the query bank from a large keyword-research workbook without testing every row;
  • why results can differ so much between engines, and what that means for a small organisation;
  • the on-site changes I recommended, and the order I put them in.