In San Francisco the cohort that can check our roofing band is tiny — and that is the finding
Published 2026-08-05 · records retrieved 2026-08-05 · our band fixed as of 2026-07 · how we build these numbers
Run your own numbers in the roof replacement calculator →
This file should have been the good one. San Francisco publishes every building permit it has issued, and 5,527 filed since the start of 2025 describe a re-roof, each carrying a declared construction cost. Unlike some jurisdictions, those figures behave like numbers people typed rather than numbers a table produced: the most common value accounts for just 5.20% of the cohort, the five most common together for 19.22%. That is a distribution, not a fee schedule.
Then you go looking for the denominator. Our calculator prices roofing per square — 100 square feet of roof surface — so checking it against a permit means knowing how big the roof was. In this file, 5.74% of re-roof permits say. The other 94% describe the work without stating its size, and everything below follows from that missing column.
1. What this record is
The dataset is Building Permits, published by the San Francisco Department of Building Inspection on the city's open-data portal, resource i98e-djp9. We retrieved it on 2026-08-05; the portal reports its rows were last refreshed 2026-08-04. Five fields do the work: permit_number, filed_date, description, estimated_cost, revised_cost. The portal's metadata sets licenseId to PDDL — a public-domain dedication — with the attribution field null, so derived statistics carry no licence condition. The raw rows do carry street addresses and coordinates; we publish aggregates and the queries that made them, never rows that identify a building.
# licence and freshness, straight from the portal metadata
curl -s 'https://data.sfgov.org/api/views/i98e-djp9.json' | python3 -c \
"import sys,json; m=json.load(sys.stdin); print(m['licenseId'], m['attribution'])"
# => PDDL None
# the window we use
curl -s -G 'https://data.sfgov.org/resource/i98e-djp9.json' \
--data-urlencode '$select=count(*) as n' \
--data-urlencode "\$where=filed_date >= '2025-01-01T00:00:00'"
# => [{"n":"39514"}]
We then pulled all 39,514 rows in that window, 5,000 at a time, and did every count below locally. The address and coordinate columns are deliberately not selected:
curl -s -G 'https://data.sfgov.org/resource/i98e-djp9.json' \
--data-urlencode '$select=permit_number,filed_date,description,estimated_cost,revised_cost' \
--data-urlencode "\$where=filed_date >= '2025-01-01T00:00:00'" \
--data-urlencode '$order=:id' --data-urlencode '$limit=5000' --data-urlencode '$offset=0'
2. What it measures — and what it quietly does not
A row is not a permit. The portal labels each row Permit at an address, and it means it: 5,874 rows match our re-roof pattern but carry only 5,527 distinct permit numbers, because a permit spanning two adjacent addresses is filed once per address. Anyone who runs count(*) here and reports permits is over by 6.3%.
The dollar field is declared, not settled. revised_cost looks like it should record what the job became, but it differs from estimated_cost on 1.16% of permits — overwhelmingly a copy, not a corrected outturn. More telling: the figure does not respond to what is being installed. Permits naming only flat systems, permits naming only pitched materials, and permits naming both all return the same median, $17,000, identical across all three. Torch-down and architectural shingle do not cost the same per foot in the real world. A number that cannot tell them apart is a valuation filed for the paperwork, not a bid.
The precision is two digits at best. 53.5% of the usable figures are exact multiples of $1,000 and 18.6% are multiples of $5,000 — self-reported, rounded by the person filing. The median is meaningful. The individual value is not.
And the size is simply absent. Only 5.74% of the 5,527 permits — 317 of them — state an area, and only 14.85% carry any number at all in the description. One boilerplate sentence, "re-roofing: remove and replace roofing material / existing roofing material (in-kind)", accounts for 46.12% of the permits on its own, and it names no material, no area and no slope.
That boilerplate is why the cohort we would need cannot be assembled. Sorted by what the description names, 1,096 permits (19.83%) name a flat-roof system — torch-down, TPO, modified bitumen, cap sheet — 197 (3.56%) name a pitched material, 172 name both, and 4,062 (73.49%) name neither. Our calculator models a pitched roof in composition shingle. Set that 3.56% against the 5.74% that state an area and the overlap — a pitched re-roof with a stated size — is smaller than either. That is the cohort this record can offer us, and it is too small to be a test.
Two things we flagged and could not filter away. Five permits declare $1,000,000 or more, topping out at $9,682,244; at that scale the work is commercial or public rather than residential, and nothing in the file separates the two. And July 2026 does not look like the eighteen months before it: 17 re-roof rows against 320 to 390 in every preceding month. We dropped neither silently — the percentiles below are the whole cohort, tail included, which is why the table prints the full range — and we recounted with the July window closed instead.
3. The cohort, and what we dropped
The population is every row with filed_date on or after 2025-01-01: 39,514 rows. Selecting re-roofs out of it is where the counting errors live. Server-side LIKE matching is not enough: the spelling splits across reroof, re-roof and re roof, and a pattern loose enough to catch the spaced form also catches phrases where the preceding word merely ends in "re" (entire roof, where roof). So we pulled the window whole and counted locally, on a word boundary:
python3 -c "import json,re; rows=json.load(open('rows2025.json')); \
d=lambda r:(r.get('description') or '').lower(); \
S=[r for r in rows if re.search(r'\bre[\s\-]?roof', d(r))]; \
print(len(S), len({r['permit_number'] for r in S}))"
# => 5874 5527
From those 5,527 permits, the cost figure is taken as revised_cost where it exceeds $1, otherwise estimated_cost. That rule keeps 5,520 permits — 99.87% — and loses 7. The problem with this dataset is not dirty numbers. It is that the numbers have no denominator.
4. The distribution and our band on the same axis
| Quantity | Figure | What it is |
|---|---|---|
| SF declared cost — 25th percentile | $11,930 | 5,520 permits, filed 2025-01-01 onward |
| SF declared cost — median | $16,000 | same cohort |
| SF declared cost — 75th percentile | $23,685 | same cohort |
| SF declared cost — full range | $100 – $9,682,244 | same cohort; the five filings above $1M are commercial or public work, left in and visible |
| Our band — 20-square roof, national | $11,900 – $21,500 | Architectural asphalt, 10% waste, tear-off included |
| Our band — same roof, California labor index | $13,060 – $23,596 | Index 1.15 applied to the ~65% labor share |
| Roof size our band needs to contain $16,000 | 13.6 – 24.5 squares | Our own arithmetic solved backwards |
The permit interquartile range and our California-adjusted band land on top of each other. Recounting with the window closed at 2026-06-30, which drops the July anomaly, leaves 5,511 permits and the same median, $16,000. The overlap is stable. It is also, for three separate reasons, not a result.
5. Verdict
Verdict
(C) Comparison rejected — this record cannot check our band in either direction.
Our roofing band stands where it stood, fixed as of the 2026-07 stamp on our roofing data, which predates the 2026-08-05 retrieval above — and it stands on the sources in our methodology, not on anything here. The overlap in that table is real. We are not going to bank it, for three reasons.
We had to supply the roof size ourselves. Solve our band backwards and $16,000 sits inside it for anything from 13.6 to 24.5 squares — a window covering most single-family roofs in the city. A check that a window this wide passes could not have failed, and a check that could not have failed has confirmed nothing.
The cohort is mostly not what we model. Our band prices pitched composition shingle; 3.56% of these permits name a pitched material against 19.83% naming a flat system, and nearly three quarters name neither. Two distributions sharing an order of magnitude is not evidence that either describes the other — and the material medians above, identical at $17,000 whether the description says flat, pitched or both, say the permit figure was never sensitive to that difference at all.
Our geographic adjustment cannot see this city. The 1.15 index is a statewide California figure derived from BLS state median wages for roofers. A statewide median cannot represent one metropolitan labor market, and we hold no city-level wage data for San Francisco. That is the weakest arithmetic here — and it is the step that moved our band onto the permit range.
6. If you are collecting roof quotes in San Francisco
Get the square count in writing, because the city will not have it. No amount of public-records work will hand you a comparable rate here. The square count has to come from your bidders, and it is the one number that makes competing bids comparable at all. Our quote checklist covers the other seven line items a bid must state.
Establish flat or pitched before you compare anything, including our calculator. The city's own file leaves nearly three quarters of these jobs unclassified, and the two are different trades with different failure modes. Our roof calculator prices pitched roofs; if yours is flat or low-slope with a torch-down or single-ply membrane, our band is not the right reference, and you should say so to anyone who quotes it at you.
And do not read the permit's dollar figure as anyone's price. It is a declared valuation, an exact multiple of $1,000 more than half the time, that does not change when the material changes. If a cost website quotes you a San Francisco roof price sourced from permit records, ask which field they read. Our cost-factors guide covers what actually moves the number.
Frequently asked questions
Can I find out what a neighbor paid for their roof from San Francisco permits?
Not usefully. The dollar figure is a declared construction cost that behaves like a rounded number rather than a contract total — 53.5% of the usable figures are exact multiples of $1,000. It also does not respond to what is being installed: permits naming flat systems, pitched materials, or both all carry the same median, $17,000. And because 94% never state the roof's size, you cannot divide two neighboring filings down to a comparable rate either.
Why can't you calculate a cost per square from San Francisco permit data?
Because the denominator is missing. Only 5.74% of the 5,527 re-roof permits state a roof area, and only 14.85% carry any number at all in the description. A per-square rate built from fewer than one permit in seventeen would describe that sliver rather than the city, so we have not published one — not a median, not a range.
Is $16,000 a typical San Francisco roof price?
It is the median declared cost on a re-roof permit, which is a different claim. Read against our own numbers, $16,000 sits inside our California-adjusted band for a roof anywhere between about 13.6 and 24.5 squares — wide enough to cover most single-family roofs in the city, which is why the overlap proves nothing. Treat it as an order of magnitude, not a quote.
Why does this record recount without July 2026?
Because that month does not look like the eighteen before it: re-roof filings run between 320 and 390 a month through June 2026, then show 17 rows in July. The file alone cannot say whether something changed in how filings reach it or the month has simply not landed, so we recounted with the window closed at 2026-06-30. That leaves 5,511 permits and the same median, $16,000.