Measuring small-business web presence without Google Places.
I measured the web presence of every HVAC service firm in Mobile and Baldwin County, Alabama, without touching Google Places. The frame came from Overture Maps; the counties came from a spatial join against Census boundaries. Then I broke my own first answer three times. Each correction is documented below with the before and after, because the corrections are worth more than the rate.
N = 119 firms · Mobile 53 · Baldwin 66 · measured August 14, 2026 · 95% Wilson intervals · aggregate data CSV
Three claims on this page did not survive the scale-up.
This pilot published on 2026-08-14. A day later I ran the same method across 11 verticals and 1,916 firms in the same two counties, and it contradicted three things stated here. The corrections are below, dated, with what was claimed, what is actually true, and how it was found. Nothing was quietly edited out. The full study is at Small-business web presence across eleven verticals.
| What this page claimed | The correction | How it was found | What changed here |
|---|---|---|---|
| A median mobile PageSpeed score of 66, published as a point estimate and used as a section headline. | A single-run Lighthouse score is not stable enough to support a published median to that precision. Re-measuring the same 73 HVAC URLs one day later moved 37 of them by 5 points or more and 15 by 10 points or more, across a range of −30 to +22. The median across the pair barely moved, which is the point: lab scores are stable in aggregate and noisy per site, and 66 was stated more precisely than the method allowed. | The eleven-vertical scale-up re-scored the same URLs a day apart, which made a test-retest comparison available that the pilot never ran on itself. | The median is still published, now alongside the re-measurement distribution. Section 08 was rewritten around the instability rather than the number. |
| 21.8% of firms have no website, framed as a floor for local businesses generally. | 21.8% is an HVAC number, and HVAC is one of the better verticals. Across eleven verticals the no-web-presence rate is 31.4% [29.4–33.5]. Law is the best at 14.0% and barber & salon the worst at 48.1%. Anyone who read this article as “about a fifth of local businesses have no website” was generalising a single trade. | Measuring ten more verticals on an independently rebuilt frame in the same two counties. | The rate is unchanged and still correct for HVAC. Every place it appears is now scoped to HVAC explicitly, and the market-wide figure links to the full study. |
| Naive liveness checking overstates breakage by 89%, published as a section headline. | 89% is what the correction was worth on 119 HVAC firms. At 1,916 firms across eleven verticals the same correction is worth 60% — a naive single-client check calls 29.2% of sites broken against a real figure of 18.3%. The direction and the lesson hold. The magnitude does not generalise, and this article's headline figure was itself high. | Running the identical three-stage liveness correction at fifteen times the sample size. | The 89% stays, scoped to HVAC, with the at-scale figure printed next to it. |
Why the wrong numbers are still on the page
Deleting them would be tidier and worse. The rates below are what this frame and this method produced on 119 HVAC firms, and they are still correct for HVAC. What was wrong was the scope one of them was read at, the precision another was stated to, and the generality claimed for a third. Each of those is now marked where it appears, rather than being replaced by a number this pilot never measured.
Section 08 got the largest rewrite. The median PageSpeed score used to be its headline; the measured instability of that median is a better finding than the median was, and it is now the section.
Five numbers, with their error bars attached.
At N = 119 the interesting quantity is not any single rate. It is how far each rate moved when I fixed a measurement defect. The table below is the corrected position; the sections after it are how each correction was found and what it was worth.
| Measure | Count | Rate | 95% Wilson interval |
|---|---|---|---|
| Firms with no website of their own A floor, not a point estimate — see the frame-bias section. | 26 / 119 | 21.8% | 15.4% – 30.1% |
| Sites that do not load, among firms that have one After all three liveness corrections. The uncorrected figure was 18.3%. | 9 / 93 | 9.7% | 5.2% – 17.4% |
| Firms with no working web presence (none, or one that fails to load) | 35 / 119 | 29.4% | 22% – 38.1% |
| Origins with any Chrome UX Report field data 96.8% of these origins are below Google's traffic threshold for field data. | 3 / 93 | 3.2% | 1.1% – 9.1% |
| Sites missed by the first pass and recovered by an independent second search A null result. Zero recovered, from 26 firms re-searched independently. | 0 / 26 | 0% | 0% – 12.9% |
Every interval on this page is a Wilson score interval at 95%. Wilson rather than the textbook normal approximation because several of these proportions sit close enough to a boundary that the normal interval runs below zero, which is not a defensible thing to print.
96.8% of these origins have no field data at all.
Google publishes the Chrome User Experience Report: real performance data from real Chrome users, aggregated per origin. An origin only appears in it once it clears a traffic threshold. Of the 93 origins in this study, 3 have CrUX data. 90 do not — 96.8% [90.9–98.9], with the reason returned by the API being, in every case, insufficient traffic.
That reframes the entire study. I went in expecting to find slow websites. What the data mostly shows is websites nobody visits. A site can be perfectly built and still sit below the threshold, because the constraint is demand, not code. Rebuilding a site that nobody reaches produces a faster site that nobody reaches.
This is the finding that most directly undercuts what I sell. I am publishing it in the second section rather than a footnote because a study that only produces conclusions flattering to its author is not a study.
What I will not claim
I am not publishing a Core Web Vitals failure rate for this population, and neither should anyone else working from this kind of data. Core Web Vitals is officially assessed on field data. 96.8% of these origins have none. The assessment is not bad, or borderline, or failing — it does not exist. Lab LCP from a Lighthouse run is a diagnostic signal about one simulated load, not a verdict on a metric that is defined as the 75th percentile of real users.
The three origins that do carry field data are reported in the dataset with their LCP, CLS, INP and TTFB at the 75th percentile. Three is not a sample. It is three.
21.8% of HVAC contractors have no website. Read that as a floor.
Corrected 2026-08-15 — this is an HVAC number
When this section first published it framed 21.8% as a floor for local businesses. It is a floor for HVAC contractors in these two counties, and HVAC turns out to be one of the better verticals. Across 11 verticals and 1,916 firms in the same counties, the no-web-presence rate is 31.4% [29.4–33.5]. Law is the best at 14.0%. Barber & salon is the worst at 48.1%. HVAC's 26.3% on the rebuilt frame sits sixth of eleven — squarely in the better half.
Anyone who read this article as “about a fifth of local businesses have no website” was reading a single trade as a market. The market picture is in the eleven-vertical study.
Overture Maps is assembled from business-listing feeds. A company enters this frame by having been listed somewhere first. That is a selection rule, and it selects in exactly the direction that matters here: a firm with no website, no listing, and no online footprint of any kind cannot appear in the denominator, because nothing put it there.
So the 26 of 119 firms with no site are the ones who are listed somewhere and still have no site. The firms invisible to the frame are disproportionately the ones with the least digital presence. 21.8% is a lower bound on the true rate among all operating HVAC contractors in these two counties, and the gap between the two is a quantity this method cannot estimate.
That last sentence has since been improved on. The scale-up bounded the frame against Census County Business Patterns, BLS QCEW and Census Nonemployer Statistics, and it also measured a second bias running the other way — a 30.5% recall miss over the no-presence stratum, mostly booking-platform pages the listing feeds never carry. Two biases, opposite directions, one measured and one only bounded. That is why the full study refuses to publish a single point estimate for local business as a whole.
Fold in the sites that exist but do not load and 35 of 119 firms — 29.4% [22–38.1] — have no working web presence at all. That figure inherits the same floor property, for the same reason.
The interval — 21.8% [15.4–30.1] — does not help with this. Wilson intervals describe how much a number would move under resampling from the same frame. They are silent about whether the frame is the right one. On this study the frame bias is almost certainly the larger error, and it points in a known direction, which is the most useful thing I can say about it.
Naive liveness checking overstated breakage by 89% here.
The first pass did what most audits do: one HTTP request per site, score anything that is not 2xx or 3xx as broken. That produced 17 broken sites out of 93, or 18.3%. The final figure, after three corrections, is 9 out of 93 — 9.7%. The uncorrected number was 89% too high.
Corrected 2026-08-15 — the magnitude does not generalise
This section originally carried 89% as a headline about liveness checking in general. It is what the correction was worth on 119 HVAC firms. Run the identical three-stage correction across 11 verticals and 1,916 firms and it is worth 60% — a naive check calls 29.2% of sites broken against a real figure of 18.3%.
The direction and the lesson hold at both scales, and the lesson is the transferable part: one unretried request per site is not a measurement. The 89% figure was vertical-specific and was stated here as though it were not.
| Stage | Broken | Rate | 95% CI | What the stage adds |
|---|---|---|---|---|
| 1. Status code only | 17 / 93 | 18.3% | 11.7–27.3% | One unretried GET per site with a default user-agent. Anything that isn't 2xx or 3xx is scored broken. |
| 2. + browser headers, www/apex, http/https | 15 / 93 | 16.1% | 10–24.9% | Retry with a full browser header set, then try the www/apex counterpart and the other scheme. Recovered two sites — one 406, one 404 that resolved on its host variant. |
| 3. + bot-gate classification | 11 / 93 | 11.8% | 6.7–20% | A 403 carrying a WAF fingerprint (cf-mitigated header, challenge body) is a live site refusing a robot, not a dead site. Four reclassified. |
| 4. + PageSpeed Insights as independent arbiter | 9 / 93 | 9.7% | 5.2–17.4% | Real Chrome on Google's network, a second client with a different IP, TLS stack and rendering engine. Overturned two more — both sites load fine and score 100 and 97. |
The fourth stage is the one worth stealing. PageSpeed Insights runs real Chrome from Google's network. Pointing it at the same URL gives a second, genuinely independent client: different IP, different TLS stack, different rendering engine, run by a party with no stake in my answer. Where the two clients disagree, that disagreement is information, and it is free.
| Artifact | What was actually happening |
|---|---|
| A bot gate that returns HTTP 200 | One origin answered 200 OK with a 1,954-byte body titled “Checking your browser” — a JavaScript interstitial. Every status-code-only check scores that site perfectly healthy. Nothing about the response distinguishes it from a real homepage except reading the body. |
| A TLS stack that invents failures | The first liveness pass ran on macOS system Python against LibreSSL and returned TLSV1_ALERT_PROTOCOL_VERSION on 6 hosts. curl, on the same machine, reached all 6 on its unretried first request — 3 served a normal page, 3 served a bot challenge. Zero were actually broken. The measurement tool was the defect. |
| Bot walls are the single largest source of false failures | That same first pass recorded 20 hosts as 403 Forbidden. All 20 were live: 18 answered normally once a browser header set was attached, and the remaining 2 were bot-gated pages on functioning sites. |
| DNS failures, by contrast, were real | 8 hosts failed name resolution on the first pass. curl failed on all 8 too. When two independent clients agree a domain does not resolve, it does not resolve — the correction below is not a licence to explain away every negative. |
| One firm's two brands, joined by a 301 | hansenair.com returns a 301 to hansensupertechs.com. Two apparently distinct records in the source data are one company. That merge was found by following redirect chains in the liveness pass, not by any string-matching rule — which is the point: some duplicates are only visible from the network layer. |
The arbiter disagreed in both directions
Reporting only the disagreements that lower my number would be cherry-picking, so here are both. PageSpeed Insights overturned 2 sites my checker called broken — they load fine and score 100 and 97. It also contradicted 2 sites my checker called live, returning NO_FCP: Chrome loaded them and nothing painted.
The rule I committed to before seeing the results is that a site counts as live if either independent client reached it, so those two stayed in the live column. Under the stricter rule — broken if either client fails — the rate would be 11/93 = 11.8%. Both figures are in the dataset. Pick the rule that suits your question, but pick it before you look.
Place data is one record per listing, not one per business.
The query returned 175 candidate records. After exclusions, 145 records remained. Those 145 records are 119 businesses. One firm appeared 9 times — one record per service-area pin across six localities.
Counting records instead of firms inflates N by 47%. That alone would be a citation-integrity problem. The worse part is that it also biases the headline in a specific direction, because duplication is not random.
| Source records for the firm | Firms | Share with a website |
|---|---|---|
| 1 | 105 | 75.2% |
| 2 | 8 | 100% |
| 3 | 5 | 100% |
| 9 | 1 | 100% |
Every one of the 14 multi-listing firms in this frame has a website. 75.2% of the 105 single-listing firms do. Multi-location operators duplicate the most and are the most likely to have a site, so record-weighting quietly overstates web presence: the no-website rate comes out at 17.9% per record versus 21.8% per firm. Nearly four points, in the flattering direction, from a decision most people never notice they made.
How the linkage works, and the trap inside it
Union-find over three keys: last-ten-digits phone, a normalised name with legal suffixes and branch qualifiers stripped, and registrable domain. Records sharing any key collapse into one firm.
Manufacturer and directory domains have to be excluded from the domain key. Otherwise any two records carrying the same manufacturer URL collapse into one business and the frame silently loses a firm. This is not hypothetical here: trane.comappears on two candidate records — one contractor, one manufacturer supply office — and without the exclusion they would have merged. The same list does double duty: it is what identifies the listings whose website field points at something that is not the firm's site.
One merge could not be found by any string rule. hansenair.com returns a 301 to hansensupertechs.com — two brands, one company, visible only from the network layer. It surfaced while following redirect chains during the liveness pass. String matching gets you most of the way and then stops.
The websites field is wrong in both directions at once.
Every place dataset ships a website field, and it is the obvious thing to count. Counting it gets the headline wrong twice over.
| Error | Count | Effect on the headline |
|---|---|---|
| Blank, but the firm owns a working site | 8 of 29 blanks (27.6%) | Overstates the no-website rate. |
| Populated with a manufacturer or directory URL | 5 of 119 firms (4.2%) | Understates it, and pollutes any liveness or speed measurement taken from the field. |
29 of 119 firms (24.4% [17.5–32.8]) had a blank website field. A structured search pass — name, city, phone, likely domain guesses, chamber and BBB profiles — found real, firm-owned sites for 8of them. Every recovered domain had to clear a verification step before it counted: fetch the homepage and the contact page, then match the listing's phone digits, street number and distinctive name tokens against the page text. Guessing a plausible domain is not evidence that it belongs to the business.
In the other direction, 5listings carried a URL that is not the firm's website at all: ruud.com, rheemac.com, trane.com, americanstandardair.com, and an hvacnearyou.com profile page. Score those as “has a website” and you have credited a contractor with a manufacturer's marketing site. Run a speed test on them and the number you get describes a national brand's CDN, not a local business.
After both corrections the resolved position is 93 firms with an own website (85 from the listing field, 8 recovered by search) and 26 without one.
Getting the category filter right is harder than it looks.
The intuitive way to enumerate a trade is a keyword filter over the category field. It fails in both directions, and both failures are measurable here.
False positives. A regex including duct — a reasonable token for an HVAC filter — matches 61 records inside the bounding box. 2 of them are HVAC. The rest are broadcasting_media_production (27), music_production (17), beauty_product_supplier (6), and so on down. “Duct” is a substring of “production” and “products.” That is a 96.7% false positive rate on one token.
False negatives. Filtering on the primary category alone is the other trap, because the source data miscategorises real contractors. 10 of the 119 firms in the final frame — 8.4% — carry a primary category that is not hvac_services. One HVAC contractor is filed under electrician, another under plumbing, one under truck_dealer. A primary-category-only filter drops all of them.
What I did instead: select on the primary category, the alternate-category list, and a short set of adjacent primaries, then adjudicate every record from the wider tiers by hand with a written reason. Thirty records were excluded that way, and eleven were affirmatively kept. The reason codes ship in the dataset so anyone who would draw the line differently can say exactly where.
| Exclusion code | Records | Rule |
|---|---|---|
DISTRIBUTOR | 13 | Wholesale supply house selling to the trade, not a contractor a customer hires. |
NOT_HVAC | 7 | The HVAC tag is spurious; the core business is another trade or a consultancy. |
NON_BUILDING | 6 | Automotive, marine, transport refrigeration, or ice — does not service building HVAC. |
MANUFACTURER | 2 | Makes equipment; not a local service business. |
CHAIN | 1 | National brand operating on a corporate site. |
RENTAL | 1 | Temporary climate-equipment rental. |
A single-run Lighthouse score is not a point estimate.
This section used to lead with a median mobile score of 66. That median is still below, and it is still what 82 scored sites produced. What it is not is a number precise enough to headline, and the evidence for that came from re-running the measurement.
73 of these HVAC firms were scored again one day later, on the same URLs, by the same API, as part of the eleven-vertical study. The median across the matched pairs moved from 66 to 65. Underneath that, individual sites moved a long way.
| Absolute change, one day apart | Sites | Share of the 73 |
|---|---|---|
| No change | 13 | 18% |
| 1–2 points | 13 | 18% |
| 3–4 points | 10 | 14% |
| 5–9 points | 22 | 30% |
| 10–19 points | 10 | 14% |
| 20 points or more | 5 | 7% |
Median change 0 points, mean 0.4, range -30 to +22. 37 of 73 moved by 5 points or more, 15 by 10 or more, 5 by 20 or more.
Corrected 2026-08-15 — what this means
A Lighthouse performance score is one synthetic run against variable network and origin conditions. It is stable in aggregate and noisy per site. Half of these sites moved by five points or more in twenty-four hours, and 11 of 73 crossed a Lighthouse band boundary — 6fell from “needs improvement” into “poor” and 5 climbed the other way. Nothing about the sites changed. A report that graded those 11 businesses on Tuesday and again on Wednesday would have given them different verdicts.
So a two-point difference between medians is not a finding, and a published median should carry its distribution rather than stand alone. This page originally stated 66 to a precision its own method could not support. That is a better result than the median was: anyone running this genre of study — an agency speed audit, a market benchmark, a competitor comparison — is making the same mistake, and it takes one repeat measurement to see it.
None of this makes the distribution below wrong. It makes any single site's score, and any small difference between two aggregates, worth less than it looks.
The original run: PageSpeed Insights against all 93 resolved sites on the mobile strategy, Lighthouse 13.4.1, one run each. 82 returned a score; 11 failed, which is itself a measurement (7 could not load the document at all, 3 loaded and painted nothing).
| Statistic | Value |
|---|---|
| Minimum | 21 |
| 25th percentile | 50.25 |
| Median | 66 |
| 75th percentile | 73 |
| 90th percentile | 89.5 |
| Maximum | 100 |
| Mean | 63.6 |
| n | 82 scored of 93 attempted |
Percentiles by linear interpolation between order statistics.
| Lighthouse band | Sites | Share | 95% CI |
|---|---|---|---|
| 0–49 (Lighthouse “poor”) | 20 / 82 | 24.4% | 16.4–34.7% |
| 50–89 (“needs improvement”) | 53 / 82 | 64.6% | 53.8–74.1% |
| 90–100 (“good”) | 9 / 82 | 11% | 5.9–19.6% |
Distribution in tens: 20–29 → 2 · 30–39 → 11 · 40–49 → 7 · 50–59 → 10 · 60–69 → 16 · 70–79 → 22 · 80–89 → 5 · 90–100 → 9
The number I am deliberately not turning into a headline
75 of 82 scored sites — 91.5% [83.4–95.8] — recorded a lab LCP above 2,500 ms, with a median of 6,172 ms and a 75th percentile of 9,423 ms.
That is not a Core Web Vitals failure rate.It is a single simulated load under Lighthouse's mobile throttling, and it says nothing certain about what real users experience. Writing “91.5% fail Core Web Vitals” would be the most quotable sentence in this article and it would be false, for the reason in section 02: the assessment is field-based, and this population has no field data.
Lab CLS tells a different and more useful story — median 0.001, 75th percentile 0.031, with only 13.4% above the 0.1 threshold. Layout stability is broadly fine here. What fails is load time — images and render-blocking work, not layout.
The recall check found nothing. That is worth reporting.
A no-website finding is only as good as the search that failed to find a website. So all 26 firms in the no-site set went through a second, independent pass: fresh searches on name, city and phone in several formats, domain guesses, BBB and chamber-of-commerce profiles, Facebook and Nextdoor, averaging 3.4 distinct sources per firm.
It recovered 0 additional websites. 25 firms returned a confirmed not-found — several with a BBB profile that explicitly lists no website — and 1 had a domain listed in a chamber directory that no longer resolves. 19 of the 26 verdicts were recorded at high confidence, 7 at medium.
I expected the second pass to claw several back. It clawed back none. Miss rate 0%, 95% interval [0–12.9] — a zero numerator still leaves real room for error at n = 26, and the interval says so. The result makes the no-website finding more robust, and I would have published it either way.
The honest limit on that null
Both passes were performed by an agent, not a human. They share a failure mode: each can only see what a search index surfaces, and neither can telephone a business and ask. A firm whose site no index has crawled would be missed twice, and this check would not detect it. Two correlated passes are weaker evidence than two independent ones, and I am not going to pretend otherwise.
Promoting this pilot to a full study means calling the no-website set on the phone. That is the step that would turn the floor into an estimate.
What this study cannot support.
The frame is biased toward businesses that already have a digital presence
Overture Maps is assembled from business-listing feeds. A company enters this frame by having been listed somewhere first. A firm with no website, no listing, and no online footprint is invisible to the method entirely — it cannot appear in the denominator. That pushes the measured no-website rate DOWN. Read 21.8% as a floor. The true rate among all operating HVAC contractors in these two counties is higher by an amount this study cannot estimate.
The confidence intervals cover sampling error and nothing else
Every interval on this page is a Wilson score interval at 95%. It answers one question: if this frame is the population, how much would the number move under resampling? It does not cover frame bias, category-assignment error in the source data, or my own inclusion judgments. The frame bias is almost certainly larger than any interval shown.
The recall check was performed by an agent, not a human
The second-pass search for missed websites was run by a language agent with web search and fetch, averaging 3.4 independent sources per firm. Both passes share the same failure mode: they can only find what a search index surfaces. Neither can phone a business and ask. A business with a site that no index has crawled would be missed twice, and the null result below would not detect it. A full study requires human phone validation of the no-website set.
N = 119 is a pilot, not a census of anything larger — and one trade is not a market
One trade, two counties, one day. The width of the intervals is the honest read: the no-website rate is somewhere in a 15-point band. This study is worth publishing for its method, not for its headline rate — the corrections are the transferable part. Scaling to eleven verticals and 1,916 firms confirmed the sharper version of that warning: HVAC is one of the better verticals, and its rate is not a stand-in for local business. See the correction notice at the top of the page.
Every inclusion and exclusion is a judgment I made
Thirty records were removed by hand, each with a written reason code. Eleven Tier B and C records were affirmatively kept for the same reason. Someone else drawing the contractor/distributor line differently would get a different N. The reason codes are published in the CSV so the disagreement can be specific.
One speed run per origin, homepage only — and the run-to-run variation is now measured
Lighthouse lab scores vary run to run. A single run per site is enough for a distribution and not enough for any individual verdict. That was stated here as an assumption when this pilot published; it is now measured. Re-scoring 73 of these same URLs one day later moved 37 of them by 5 points or more and 15 by 10 or more, across a range of −30 to +22. See the correction notice and section 08. Only homepages were measured; interior pages are frequently slower.
Everything needed to run it again.
The frame cost nothing and required no account. Overture Maps publishes its releases as Parquet on an anonymous S3 bucket; DuckDB reads them in place. The bounding-box pull returned 36,852 places in roughly 8 seconds on a laptop.
| Parameter | Value |
|---|---|
| Source | Overture Maps Foundation, theme = places overturemaps.org |
| Release | 2026-07-22.0 |
| Access | Anonymous S3, no key, no account, no rate limit |
| Engine | DuckDB 1.4.5 with the spatial extension |
| Bounding box | lon −88.50 to −87.35 · lat up to 31.40 Returned places span −88.49986 to −87.35004 lon and 30.22489 to 31.39923 lat. Three of the four edges are clamped by the filter. The southern edge is not: it sits offshore in the Gulf, so any ymin at or below 30.22 reproduces the identical set. |
| Places in the box | 36,852 Pulled in roughly 8 seconds. |
| County assignment | Spatial join against TIGER/Line 2024 county polygons STATEFP 01; COUNTYFP 097 (Mobile) and 003 (Baldwin). Not Overture's locality strings. |
| Join result | Mobile 18,805 · Baldwin 15,168 · outside both 2,879 The 2,879 (7.8%) are inside the rectangle but outside both counties — Mississippi, Florida, and the Gulf. A locality-string filter would have kept an unknown share of them. |
| Record filters | operating_status ≠ permanently_closed; confidence ≥ 0.6 |
| Speed measurement | PageSpeed Insights API, mobile strategy, Lighthouse 13.4.1, one run per origin |
| Field data | CrUX API, origin-level query, 28-day window ending 2026-08-13 |
The county assignment is worth isolating, because it is the step people skip. Overture records carry a locality string, and using it is tempting. I did a point-in-polygon join against TIGER/Line 2024 county boundaries instead. 2,879 of the 36,852 places in the rectangle — 7.8% — fall outside both county polygons. They are in Mississippi, in Florida, or in the water. A locality-string filter would have kept an unknown share of them and quietly changed the denominator.
-- 1. Pull the bounding box straight from Overture's public S3 bucket.
INSTALL spatial; LOAD spatial;
CREATE TABLE bbox_places AS
SELECT id, names.primary AS name,
categories.primary AS cat_primary,
categories.alternate AS cat_alt,
confidence, websites, phones, addresses,
operating_status,
ST_X(ST_GeomFromWKB(geometry)) AS lon,
ST_Y(ST_GeomFromWKB(geometry)) AS lat
FROM read_parquet(
's3://overturemaps-us-west-2/release/2026-07-22.0/theme=places/type=*/*',
hive_partitioning = 1)
-- The southern edge sits offshore, so it does not bind: any ymin at or
-- below 30.22 returns the identical 36,852 places.
WHERE bbox.xmin > -88.50 AND bbox.xmax < -87.35
AND bbox.ymin > 30.10 AND bbox.ymax < 31.40;
-- 2. Assign county by POINT-IN-POLYGON against TIGER/Line 2024,
-- not by trusting the locality string on the record.
CREATE TABLE bbox_county AS
SELECT p.*, c.county_name AS county_tiger
FROM bbox_places p
LEFT JOIN counties c
ON ST_Contains(c.geom, ST_Point(p.lon, p.lat));
-- 3. Candidate selection. Three tiers, each adjudicated by hand:
-- A = primary category is hvac_services
-- B = hvac_services appears in the ALTERNATE categories
-- C = an adjacent primary category
SELECT * FROM bbox_county
WHERE county_tiger IS NOT NULL
AND (operating_status IS NULL OR operating_status <> 'permanently_closed')
AND confidence >= 0.6
AND ( cat_primary = 'hvac_services'
OR list_contains(cat_alt, 'hvac_services')
OR cat_primary IN ('air_duct_cleaning_service',
'hvac_supplier',
'commercial_refrigeration') );Downstream of the query, five scripts do the rest: frame construction and record linkage, two-client liveness measurement, domain verification for search-recovered sites, the PageSpeed Insights and CrUX collection, and the assembly step that computes every interval on this page. A sixth is kept deliberately — the first liveness pass, LibreSSL bug intact, because the artifact it produced is half of section 04. They are not polished. They are the scripts that produced these numbers. If you want them for a replication, email me and I will send them.
Take the data.
The aggregate dataset behind every number on this page is a single CSV: one row per measured quantity, with section, numerator, denominator, value, unit, and both bounds of the 95% Wilson interval where a proportion is involved.
Download cds-hvac-web-presence-pilot-2026-08-14.csv
Why the per-firm data is not published
The working files name 119 real companies alongside a judgment about whether their website works and how fast it is. Publishing that as a list would be a reputational claim about named small businesses, made by someone who sells them websites, with no right of reply and no legal review. The published file is aggregate only, and no individual firm is identifiable in it. Every finding on this page holds at the aggregate level, which is the level the findings are about.
Cite this
Campbell, Peyton. “Measuring small-business web presence without Google Places: Three corrections, and what each one was worth.” Campbell Digital Studio, 2026-08-14. www.campbelldigitalstudio.com/research/measuring-small-business-web-presence.
Dataset and article licensed CC BY 4.0. Reuse anything here with attribution, including the numbers, the corrections, and the limits — particularly the limits.
Frame data © Overture Maps Foundation, release 2026-07-22.0, used under its own licence terms. County boundaries from US Census TIGER/Line 2024. Performance measurements from the Google PageSpeed Insights and Chrome UX Report APIs.
Measured and published August 14, 2026. Corrected 2026-08-15 — see the notice at the top of the page and the corrections rows in the CSV. Corrections and replications welcome; if a number here is wrong, I would rather know.
The study that corrects this one, and the same discipline elsewhere.
The eleven-vertical scale-up that overturned three claims here, the public grading rubric, a market audit run under it, and the sourced statistics pages that follow the same rule about what a number is allowed to claim.
Want this run on your market?
The method transfers to any trade in any county — the frame is free, the measurement is free, and the corrections are the same ones. If you want the same audit for your industry and your market, or you want to argue with the numbers above, send the details. The second one is more interesting to me.