#!/usr/bin/env python3 """ Recompute the figures in "Three in four Miami addresses are not in Miami, and other things the record will not tell you" from the public bulk files. https://glvtl.com/field-study/miami-commercial-record/ By Chris Klebl, Sterling Digital Partners. Free to reuse with attribution. Standard library only. No install step. python3 count_miami.py # download both rolls, then count python3 count_miami.py nal.zip sdf.zip # count rolls you already have python3 count_miami.py --no-network nal.zip sdf.zip # skip the address-layer query Findings 02, 03 and 04 come from the Florida Department of Revenue's preliminary 2026 assessment roll for Miami-Dade (county 23). Finding 01 comes from Miami-Dade County's own address layer, queried live. The published figures came from the DOR files dated 2026-07-27 11:05 GMT (NAL) and 11:03 GMT (SDF), and from the address layer as it stood on 2026-09-20. The roll is republished each cycle and the address layer changes daily, so a later file will NOT reproduce them exactly. That is expected. What should reproduce is the method: same universe, same columns, same arithmetic. If a number moves, the date moved with it. Sources: Florida DOR, PTO Data Portal, Tax Roll Data Files https://floridarevenue.com/property/dataportal/Pages/default.aspx?path=/property/dataportal/Documents/PTO%20Data%20Portal/Tax%20Roll%20Data%20Files Miami-Dade County address layer https://gisweb.miamidade.gov/arcgis/rest/services/AddressSearchMap_PropertiesWithZip/MapServer/0 """ import csv, io, sys, zipfile, json, statistics, collections import urllib.request, urllib.parse DOR = ("https://floridarevenue.com/property/dataportal/Documents/" "PTO%20Data%20Portal/Tax%20Roll%20Data%20Files") NAL_URL = DOR + "/NAL/2026P/Dade%2023%20Preliminary%20NAL%202026.zip" SDF_URL = DOR + "/SDF/2026P/Dade%2023%20Preliminary%20SDF%202026.zip" NAL_MEMBER = "NAL23P202601.csv" SDF_MEMBER = "SDF23P202601.csv" ADDRESS_LAYER = ("https://gisweb.miamidade.gov/arcgis/rest/services/" "AddressSearchMap_PropertiesWithZip/MapServer/0/query") # DOR use codes 010-049 are commercial and industrial. CRE_LO, CRE_HI = 10, 49 # Qualification codes the appraiser treats as an open-market sale. QUALIFIED = {"0", "00", "01", "02", "03", "04", "05", "06", "07", "1", "2", "3", "4", "5", "6", "7"} SALES_FROM_YEAR = 2025 UNINCORPORATED = "UNINCORPORATED COUNTY" # Directional PREFIXES only, and only as the county abbreviates them. Miami-Dade's # grid runs its quadrant off the house number: "1251 NW 20 ST". A full word in the # trailing position -- "60 EDGEWATER DR NORTH" -- is a condominium wing label # sitting in the same column as "3A" and "PH1C", not a directional prefix, and is # deliberately NOT counted. See the method note on the page: counting every address # that maps to more than one parcel would mostly count condominium units, which is # an artifact of ownership structure rather than a trap. DIRECTIONS = {"N", "S", "E", "W", "NE", "NW", "SE", "SW"} csv.field_size_limit(1 << 24) UA = {"User-Agent": "count_miami/1.0"} def num(s): s = (s or "").replace(",", "").replace("$", "").strip() try: return float(s) except ValueError: return None def usecode(s): s = (s or "").strip() return int(s) if s.isdigit() else None def is_cre(s): c = usecode(s) return c is not None and CRE_LO <= c <= CRE_HI def rows(zf, member): """Stream one CSV member without unpacking the archive to disk.""" with zf.open(member) as fh: yield from csv.DictReader(io.TextIOWrapper(fh, encoding="utf-8", errors="replace")) def fetch(url, path=None): if path: return zipfile.ZipFile(path) sys.stderr.write("downloading %s ...\n" % url) with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=600) as r: sys.stderr.write(" Last-Modified: %s\n" % r.headers.get("Last-Modified", "not stated")) blob = r.read() return zipfile.ZipFile(io.BytesIO(blob)) def layer_count(where): q = urllib.parse.urlencode({"where": where, "returnCountOnly": "true", "f": "json"}) with urllib.request.urlopen( urllib.request.Request(ADDRESS_LAYER + "?" + q, headers=UA), timeout=120) as r: return json.load(r).get("count") def address_key(phy_addr1, zipcd): """(house number, street without its directional prefix, 5-digit ZIP), direction.""" t = " ".join((phy_addr1 or "").upper().split()).split() if len(t) < 2: return None, None house, rest, direction = t[0], t[1:], "" if rest[0] in DIRECTIONS: direction, rest = rest[0], rest[1:] if not rest: return None, None return (house, " ".join(rest), (zipcd or "").strip()[:5]), direction def main(argv): network = "--no-network" not in argv paths = [a for a in argv if not a.startswith("--")] nal = fetch(NAL_URL, paths[0] if len(paths) > 0 else None) sdf = fetch(SDF_URL, paths[1] if len(paths) > 1 else None) total = cre = uninc = uninc_cre = 0 jv_sum = av_sd_sum = av_nsd_sum = 0.0 ratios, school_equals_just = [], 0 below = collections.Counter() directions = collections.defaultdict(set) parcels_at_key = collections.defaultdict(set) cre_directions = collections.defaultdict(set) cre_parcels_at_key = collections.defaultdict(set) for r in rows(nal, NAL_MEMBER): total += 1 commercial = is_cre(r["DOR_UC"]) if (r["PHY_CITY"] or "").strip().upper() == UNINCORPORATED: uninc += 1 if commercial: uninc_cre += 1 key, d = address_key(r["PHY_ADDR1"], r["PHY_ZIPCD"]) if key: directions[key].add(d) parcels_at_key[key].add(r["PARCEL_ID"]) if commercial: cre_directions[key].add(d) cre_parcels_at_key[key].add(r["PARCEL_ID"]) if not commercial: continue cre += 1 j, s, n = num(r["JV"]), num(r["AV_SD"]), num(r["AV_NSD"]) if j: jv_sum += j if s is not None: av_sd_sum += s if n is not None: av_nsd_sum += n if j and j > 0 and n is not None: ratios.append(n / j) for thr in (99, 95, 90, 75, 50): if n < j * thr / 100.0: below[thr] += 1 if j and s is not None and s == j: school_equals_just += 1 sold = not_qualified = nominal = 0 by_code = collections.Counter() for r in rows(sdf, SDF_MEMBER): if not is_cre(r["DOR_UC"]): continue year = usecode(r["SALE_YR"]) if year is None or year < SALES_FROM_YEAR: continue sold += 1 q = (r["QUAL_CD"] or "").strip() by_code[q] += 1 if q not in QUALIFIED: not_qualified += 1 price = num(r["SALE_PRC"]) if price is not None and price <= 100: nominal += 1 # ---------- report ---------- pct = lambda n, d: "%.1f%%" % (100.0 * n / d) if d else "n/a" line = lambda k, v: print("%-48s %s" % (k, v)) print("=" * 76) print("MIAMI-DADE COUNTY COMMERCIAL PROPERTY RECORD - COUNTS") print("=" * 76) line("rows in %s" % NAL_MEMBER, f"{total:,}") line("commercial / industrial (DOR 010-049)", f"{cre:,} ({pct(cre, total)})") print("\n-- Finding 01: postal city against municipality --") line('roll parcels in "Unincorporated County"', f"{uninc:,} ({pct(uninc, total)})") line(" commercial subset", f"{uninc_cre:,} ({pct(uninc_cre, cre)})") if network: try: all_pts = layer_count("1=1") mail = layer_count("MAILING_MUNIC='MIAMI'") not_city = layer_count("MAILING_MUNIC='MIAMI' AND MUNIC_NAME<>'MIAMI'") in_city = layer_count("MUNIC_NAME='MIAMI'") line("address points in the county layer", f"{all_pts:,}") line(" postal city MIAMI", f"{mail:,} ({pct(mail, all_pts)})") line(" ... and NOT in the City of Miami", f"{not_city:,} ({pct(not_city, mail)})") line(" actually in the City of Miami", f"{in_city:,} ({pct(in_city, all_pts)})") except Exception as e: # noqa: BLE001 - report, do not crash print(" address layer unavailable: %s" % e) else: print(" (--no-network: address layer not queried)") print("\n-- Finding 02: the cap, school against non-school --") line("total just value", f"${jv_sum:,.0f}") line("total school assessed value (AV_SD)", f"${av_sd_sum:,.0f} ({pct(av_sd_sum, jv_sum)} of just)") line("total non-school assessed value (AV_NSD)", f"${av_nsd_sum:,.0f} ({pct(av_nsd_sum, jv_sum)} of just)") line("value exposed to school millage only", f"${av_sd_sum - av_nsd_sum:,.0f}") line("parcels where school assessed = just", f"{school_equals_just:,} ({pct(school_equals_just, cre)})") print("\n-- Finding 03: assessed against just value (commercial) --") line("parcels counted", f"{len(ratios):,}") line("median AV_NSD / JV", "%.1f%%" % (100 * statistics.median(ratios))) for thr in (99, 95, 90, 75, 50): line(" assessed below %d%% of just" % thr, f"{below[thr]:,} ({pct(below[thr], cre)})") line("aggregate gap (JV - AV_NSD)", f"${jv_sum - av_nsd_sum:,.0f} ({pct(jv_sum - av_nsd_sum, jv_sum)})") print("\n-- Finding 04: directional address collisions --") print(" key = (house number, street without directional prefix, 5-digit ZIP)") print(" directional prefixes counted: %s" % " ".join(sorted(DIRECTIONS))) multi = {k: v for k, v in directions.items() if len([d for d in v if d]) > 1} cre_multi = {k: v for k, v in cre_directions.items() if len([d for d in v if d]) > 1} line("distinct address keys", f"{len(directions):,}") line("keys with 2+ directional prefixes", f"{len(multi):,} ({pct(len(multi), len(directions))})") line("parcels behind them", f"{sum(len(parcels_at_key[k]) for k in multi):,}") line(" commercial keys", f"{len(cre_multi):,}") line(" commercial parcels", f"{sum(len(cre_parcels_at_key[k]) for k in cre_multi):,}") print("\n-- Finding 01: sales qualification, %d onward --" % SALES_FROM_YEAR) line("recorded sales on commercial parcels", f"{sold:,}") line(" not qualified", f"{not_qualified:,} ({pct(not_qualified, sold)})") line(" recorded at $100 or less", f"{nominal:,} ({pct(nominal, sold)})") print(" top codes:") for code, n in by_code.most_common(8): print(" %-6s %6d %s" % (code or "(blank)", n, pct(n, sold))) print("\nAggregate counts only. No owner, mailing, fiduciary, legal-description or") print("grantor/grantee field is read or printed by this script.") if __name__ == "__main__": main(sys.argv[1:])