#!/usr/bin/env python3 """ Recompute the figures in the Harris County, Texas commercial record field study (No. 04) from the Harris Central Appraisal District's public bulk files. https://glvtl.com/field-study/harris-commercial-record/ By Chris Klebl, Sterling Digital Partners. Free to reuse with attribution. Standard library only. No install step. python3 count_harris.py # download, then count python3 count_harris.py Real_acct_owner.zip Real_jur_exempt.zip python3 count_harris.py --csv harris-commercial-record-2026-09.csv python3 count_harris.py --protest Hearing_files.zip # extra, unpublished Sources -- all free, all a direct anonymous download. No login, no fee, no request form, no CAPTCHA, and no User-Agent workaround: HCAD serves the default Python-urllib User-Agent a 200. (Study No. 03's county did not, and that script had to set one. This one does not.) Harris Central Appraisal District, 2026 CAMA public data https://download.hcad.org/data/CAMA/2026/ Real_acct_owner.zip https://download.hcad.org/data/CAMA/2026/Real_acct_owner.zip 211,881,907 bytes Last-Modified: Sun, 13 Sep 2026 21:23:10 GMT members used: real_acct.txt (1,628,306 accounts, 71 columns) deeds.txt (2,554,729 conveyances, 5 columns) Real_jur_exempt.zip https://download.hcad.org/data/CAMA/2026/Real_jur_exempt.zip 114,185,783 bytes Last-Modified: Sun, 13 Sep 2026 21:24:08 GMT member used: jur_tax_dist_exempt_value_rate.txt (the taxing-district table) Hearing_files.zip https://download.hcad.org/data/CAMA/2026/Hearing_files.zip 18,009,303 bytes Last-Modified: Sun, 13 Sep 2026 21:22:49 GMT optional; --protest only; produces no published figure The directory index at https://download.hcad.org/data/CAMA/2026/ returns HTTP 500. The files at that same path return 200. A dead index is not a dead source; address the files directly, as this script does. Downloaded and counted 2026-09-20. THERE IS NO DATED OR CERTIFIED SNAPSHOT AT HCAD. Every file above is rebuilt on the district's own schedule, and the prior-year archives are not archives: on 2026-09-20 both .../CAMA/2025/Real_acct_owner.zip and .../CAMA/2024/Real_acct_owner.zip returned a Last-Modified of Sun, 13 Sep 2026 -- the same rebuild date as the 2026 file. Dallas CAD publishes DCAD2026_CERTIFIED_07232026.zip, whose filename carries its own certification date, and that is the model. HCAD has no equivalent. So: A LATER FILE WILL NOT REPRODUCE THESE FIGURES EXACTLY. That is expected and it is stated on the page. What reproduces is the method -- same universe, same columns, same arithmetic. If a number moves, the file date moved with it. AGGREGATE COUNTS ONLY. This script never reads, stores or prints an owner name, a mailing address, a grantor, a grantee or a legal description. The columns that carry them are named in SUPPRESSED below and are dropped by read_rows() before any row is counted. owners.txt is never opened at all. THE DISTRICT CLASSIFICATION IS OURS, NOT HCAD'S. HCAD publishes no "district type" column. Municipalities are identified by tax-district CODE -- the contiguous block 051-086 -- and never by name, because the names do not support it: CHELFORD CITY MUD, CLEAR BROOK CITY MUD, CLEAR LAKE CITY WA and fifteen TIRZ names all contain the word CITY and not one of them is a city. Utility districts are identified by a name pattern, which is a weaker test and is labelled as such. Run with --districts to print the full code -> name -> class table so a reader can disagree with it. "NO CITY" IS NOT "IN THE ETJ". The jurs column records taxing jurisdictions. A city's extraterritorial jurisdiction levies no tax and therefore cannot appear in it. Nothing in this script counts ETJ, and no figure it prints may be described as one. ETJ boundaries are a City of Houston GIS layer, not an HCAD column. """ import collections import csv import io import re import statistics import sys import urllib.request import zipfile BASE = "https://download.hcad.org/data/CAMA/2026/" ACCT_URL = BASE + "Real_acct_owner.zip" JUR_URL = BASE + "Real_jur_exempt.zip" HEARING_URL = BASE + "Hearing_files.zip" ACCT_MEMBER = "real_acct.txt" DEEDS_MEMBER = "deeds.txt" DIST_MEMBER = "jur_tax_dist_exempt_value_rate.txt" HEARINGS_MEMBER = "arb_hearings_real.txt" # Columns carrying personal, owner or legal-description data. Dropped on read, # never counted, never printed. owners.txt is never opened. SUPPRESSED = { "mailto", "mail_addr_1", "mail_addr_2", "mail_city", "mail_state", "mail_zip", "mail_country", "lgl_1", "lgl_2", "lgl_3", "lgl_4", "Owner_Name", "rev_by", } # Texas state property classes. F1 is commercial real, F2 industrial real. # This is the direct analogue of DOR 010-049 in studies No. 01 and No. 02 and of # land-use C/I/O in No. 03, and like those it deliberately EXCLUDES multi-family # (class A and B here) and vacant/agricultural land held for other purposes. CRE_CLASSES = ("F1", "F2") # OUR CLASSIFICATION, not HCAD's. Municipal taxing districts occupy one # contiguous code block. 069 is literally named "NOT USED" and is excluded; 077 # is absent from the district table entirely. That leaves 34 named # municipalities. Classifying by code rather than by name is the whole point -- # see the docstring for the names that would defeat a name test. CITY_CODE_LO, CITY_CODE_HI = 51, 86 CITY_CODES = {"%03d" % i for i in range(CITY_CODE_LO, CITY_CODE_HI + 1)} - {"069"} # Utility districts. This IS a name test and it is the weaker half of the # classification -- say so wherever it is used. Two patterns are published, and # the difference between them is the honest width of the judgement call. # # NARROW: municipal utility districts, water control and improvement # districts, utility districts, public utility districts. 651 of the 1,078 # districts in the table. # # BROAD: the above plus fresh water supply districts, water authorities and # drainage districts -- bodies that do the same job under a different name. # # "ID" is deliberately in NEITHER. In Harris County it is written for both # improvement and irrigation districts, and CITY OF HOUSTON ID, CITY OF LA PORTE # ID and CITY OF PASADENA ID are municipal improvement districts, not utilities. # A reader who wants them in can add the pattern; we will not guess. # # MUD needs a prefix match rather than a whole-word one because one district is # written "HC MUD492" with no space -- and its code is A52, which is also why # district codes are treated as opaque strings throughout and never as integers. UTILITY_RE = re.compile(r"\bMUD|\b(?:WCID|UD|PUD|WC&ID)\b") UTILITY_BROAD_RE = re.compile(r"\bMUD|\b(?:WCID|UD|PUD|WC&ID|FWSD|WA)\b|\bDRAINAGE\b") # An address whose first token is "0" carries no house number. HCAD writes these # for parcels it has not assigned one to -- "0 BECKER RD", "0 GREG'S WAY 1". They # are placeholders, not addresses, and they inflate any collision count, so they # are counted separately rather than silently included. PLACEHOLDER_HOUSE_NUMBER = "0" csv.field_size_limit(1 << 24) # -------------------------------------------------------------------------- # helpers # -------------------------------------------------------------------------- def num(s): s = (s or "").replace(",", "").replace("$", "").strip() if not s: return 0.0 try: return float(s) except ValueError: return 0.0 def pct(n, d): return "" if not d else "%.1f%%" % (100.0 * n / d) def bn(v): return "%.1f" % (v / 1e9) def norm_addr(s): """Uppercase, collapse runs of whitespace. Nothing else is changed. No directional prefix is stripped and no abbreviation is expanded. Study No. 02 shipped a collision finding built on directional-prefix matching and four of its headline examples turned out to be condominium wing labels. The test applied here is the strict one: two accounts collide only when HCAD wrote exactly the same site address on both. """ return " ".join((s or "").upper().split()) def is_placeholder(addr): head = addr.split(" ", 1)[0] if addr else "" return head == PLACEHOLDER_HOUSE_NUMBER def fetch(url, path=None): if path: sys.stderr.write("reading %s ...\n" % path) return zipfile.ZipFile(path), "local file, Last-Modified not observed" sys.stderr.write("downloading %s ...\n" % url) with urllib.request.urlopen(url, timeout=1800) as r: lm = r.headers.get("Last-Modified", "not stated") sys.stderr.write(" Last-Modified: %s\n" % lm) blob = r.read() return zipfile.ZipFile(io.BytesIO(blob)), lm def read_rows(zf, member): """Stream one tab-delimited member, dropping suppressed columns on the way. The files are tab-delimited with no quoting: a bare " inside a legal description would otherwise swallow the rest of the file, so QUOTE_NONE is required, not optional. Encoding is latin-1; the files are not UTF-8. """ with zf.open(member) as fh: text = io.TextIOWrapper(fh, encoding="latin-1", newline="") reader = csv.DictReader(text, delimiter="\t", quoting=csv.QUOTE_NONE) for row in reader: for col in SUPPRESSED: row.pop(col, None) yield row # -------------------------------------------------------------------------- def load_districts(jur_zip): """code -> name, from the district table. One row per district per exemption.""" names = {} rows = 0 for r in read_rows(jur_zip, DIST_MEMBER): rows += 1 code = (r.get("tax_dist") or "").strip() name = (r.get("name") or "").strip() if code: names.setdefault(code, name) return names, rows def classify(code, name): if code in CITY_CODES: return "city" if UTILITY_RE.search(name): return "utility" if UTILITY_BROAD_RE.search(name): return "utility_broad_only" return "other" def main(argv): paths = [a for a in argv if not a.startswith("--")] want_csv = None if "--csv" in argv: i = argv.index("--csv") want_csv = argv[i + 1] paths = [p for p in paths if p != want_csv] want_districts = "--districts" in argv want_protest = "--protest" in argv acct_zip, acct_lm = fetch(ACCT_URL, paths[0] if len(paths) > 0 else None) jur_zip, jur_lm = fetch(JUR_URL, paths[1] if len(paths) > 1 else None) names, dist_rows = load_districts(jur_zip) cls_of = {c: classify(c, n) for c, n in names.items()} city_codes_in_table = sorted(c for c in names if cls_of[c] == "city") utility_codes = {c for c in names if cls_of[c] == "utility"} utility_broad_codes = {c for c in names if cls_of[c] in ("utility", "utility_broad_only")} if want_districts: w = csv.writer(sys.stdout) w.writerow(["tax_dist", "name", "class"]) for c in sorted(names): w.writerow([c, names[c], cls_of[c]]) return 0 # ---------------- pass 1: real_acct.txt ---------------- all_rows = 0 all_accts = set() cre = set() cls_count = collections.Counter() in_city = no_city = 0 straddle = 0 no_jurs = 0 city_accts = collections.Counter() city_value = collections.Counter() value_total = 0.0 value_no_city = 0.0 value_in_city = 0.0 jur_counts = [] jur_counts_city = [] jur_counts_nocity = [] jur_counts_nocity_util = [] jur_hist = collections.Counter() districts_seen = set() no_city_util = 0 no_city_util_broad = 0 no_city_county_only = 0 no_city_county_only_broad = 0 util_districts_no_city = collections.Counter() util_broad_districts_no_city = set() value_no_city_util = 0.0 addr_accts = collections.defaultdict(set) addr_keymaps = collections.defaultdict(set) placeholder_accts = 0 no_keymap = 0 protested_y = 0 yr_annexed_populated = 0 new_own_dt_populated = 0 for r in read_rows(acct_zip, ACCT_MEMBER): all_rows += 1 acct = (r.get("acct") or "").strip() all_accts.add(acct) state_class = (r.get("state_class") or "").strip() cls_count[state_class] += 1 if state_class not in CRE_CLASSES: continue cre.add(acct) js = set((r.get("jurs") or "").split()) districts_seen |= js value = num(r.get("tot_appr_val")) value_total += value n = len(js) jur_counts.append(n) jur_hist[n] += 1 if not js: no_jurs += 1 cities = js & CITY_CODES if cities: in_city += 1 value_in_city += value jur_counts_city.append(n) if len(cities) > 1: straddle += 1 for c in cities: city_accts[c] += 1 city_value[c] += value else: no_city += 1 value_no_city += value jur_counts_nocity.append(n) u = js & utility_codes if u: no_city_util += 1 value_no_city_util += value jur_counts_nocity_util.append(n) for d in u: util_districts_no_city[d] += 1 else: no_city_county_only += 1 ub = js & utility_broad_codes if ub: no_city_util_broad += 1 util_broad_districts_no_city |= ub else: no_city_county_only_broad += 1 addr = norm_addr(r.get("site_addr_1")) km = (r.get("key_map") or "").strip() if not km: no_keymap += 1 if addr: if is_placeholder(addr): placeholder_accts += 1 addr_accts[addr].add(acct) if km: addr_keymaps[addr].add(km) if (r.get("protested") or "").strip() == "Y": protested_y += 1 if (r.get("yr_annexed") or "").strip(): yr_annexed_populated += 1 if (r.get("new_own_dt") or "").strip(): new_own_dt_populated += 1 n_cre = len(cre) # ---------------- pass 2: deeds.txt ---------------- deed_rows = 0 deed_cols = [] deed_rows_cre = 0 deed_accts_cre = set() deed_dos_blank = 0 deed_years = collections.Counter() with acct_zip.open(DEEDS_MEMBER) as fh: text = io.TextIOWrapper(fh, encoding="latin-1", newline="") reader = csv.DictReader(text, delimiter="\t", quoting=csv.QUOTE_NONE) deed_cols = list(reader.fieldnames or []) for row in reader: deed_rows += 1 a = (row.get("acct") or "").strip() dos = (row.get("dos") or "").strip() if not dos: deed_dos_blank += 1 if a in cre: deed_rows_cre += 1 deed_accts_cre.add(a) if len(dos) >= 4: deed_years[dos[-4:]] += 1 # every deeds.txt column that could carry a price, consideration, # qualification code or instrument type. The answer is the finding. PRICE_WORDS = ("price", "consid", "amount", "amt", "sale_", "value", "val", "qual", "instr", "stamp", "transfer_tax") price_cols = [c for c in deed_cols if any(w in c.lower() for w in PRICE_WORDS)] # ---------------- addresses ---------------- shared = {a: s for a, s in addr_accts.items() if len(s) > 1} shared_accts = sum(len(s) for s in shared.values()) shared_real = {a: s for a, s in shared.items() if not is_placeholder(a)} shared_real_accts = sum(len(s) for s in shared_real.values()) dispersed_groups = [a for a in shared if len(addr_keymaps[a]) > 1] dispersed_accts = sum(len(shared[a]) for a in dispersed_groups) dispersed_real = [a for a in dispersed_groups if not is_placeholder(a)] dispersed_real_accts = sum(len(shared[a]) for a in dispersed_real) largest = max((len(s) for s in shared.values()), default=0) # ---------------- output ---------------- out = [] def add(metric, value, denom="", share="", note=""): out.append([metric, value, denom, share, note]) add("hcad_accounts_total", len(all_accts), "", "", "rows in real_acct.txt = %d; acct is unique" % all_rows) add("commercial_accounts_f1_f2", n_cre, len(all_accts), pct(n_cre, len(all_accts)), "state_class F1 (commercial real) %d + F2 (industrial real) %d" % (cls_count["F1"], cls_count["F2"])) add("commercial_appraised_value_usd", int(value_total), "", "", "sum of tot_appr_val over F1/F2") # finding 01 -- jurisdiction add("taxing_districts_in_table", len(names), "", "", "distinct tax_dist in jur_tax_dist_exempt_value_rate.txt " "(%d rows, one per district per exemption code)" % dist_rows) add("taxing_districts_on_commercial", len(districts_seen), len(names), pct(len(districts_seen), len(names)), "distinct codes appearing in jurs on an F1/F2 account") add("city_codes_in_block_051_086", len(city_codes_in_table), "", "", "OUR classification: contiguous code block, 069 NOT USED excluded, " "077 absent from the table") add("cities_holding_commercial", len(city_accts), len(city_codes_in_table), pct(len(city_accts), len(city_codes_in_table)), "city codes appearing on at least one F1/F2 account") add("commercial_in_no_city", no_city, n_cre, pct(no_city, n_cre), "no tax_dist in 051-086 on the account. NOT an ETJ count: jurs records " "taxing jurisdictions and an ETJ levies no tax") add("commercial_in_a_city", in_city, n_cre, pct(in_city, n_cre), "splits exactly: %d + %d = %d" % (no_city, in_city, n_cre)) add("commercial_in_two_cities", straddle, in_city, pct(straddle, in_city), "accounts carrying more than one 051-086 code") add("commercial_with_no_jurs_at_all", no_jurs, n_cre, pct(no_jurs, n_cre), "jurs empty; counted in no_city above") add("value_no_city_usd", int(value_no_city), int(value_total), pct(value_no_city, value_total), "$%sbn of $%sbn" % (bn(value_no_city), bn(value_total))) add("value_in_city_usd", int(value_in_city), int(value_total), pct(value_in_city, value_total), "") add("houston_accounts", city_accts["061"], n_cre, pct(city_accts["061"], n_cre), "tax_dist 061 CITY OF HOUSTON") add("houston_value_usd", int(city_value["061"]), int(value_total), pct(city_value["061"], value_total), "$%sbn of $%sbn" % (bn(city_value["061"]), bn(value_total))) for code, cnt in city_accts.most_common(6)[1:6]: add("city_accounts_%s" % names[code].lower().replace(" ", "_"), cnt, n_cre, pct(cnt, n_cre), "tax_dist %s" % code) add("no_city_in_utility_district", no_city_util, n_cre, pct(no_city_util, n_cre), "in no city AND in a MUD / WCID / UD / PUD. Utility class is a NAME " "test, weaker than the city code block") add("no_city_county_only", no_city_county_only, n_cre, pct(no_city_county_only, n_cre), "in no city and in no utility district") add("value_no_city_in_utility_district_usd", int(value_no_city_util), int(value_no_city), pct(value_no_city_util, value_no_city), "$%sbn of the $%sbn outside every city" % (bn(value_no_city_util), bn(value_no_city))) add("no_city_in_utility_district_broad", no_city_util_broad, n_cre, pct(no_city_util_broad, n_cre), "the same count with fresh water supply districts, water authorities " "and drainage districts added. The gap between this and the narrow " "figure is the width of the judgement call") add("no_city_county_only_broad", no_city_county_only_broad, n_cre, pct(no_city_county_only_broad, n_cre), "") add("utility_districts_holding_no_city_commercial", len(util_districts_no_city), len(utility_codes), pct(len(util_districts_no_city), len(utility_codes)), "of %d utility districts in the table" % len(utility_codes)) add("utility_districts_holding_no_city_commercial_broad", len(util_broad_districts_no_city), len(utility_broad_codes), pct(len(util_broad_districts_no_city), len(utility_broad_codes)), "of %d on the broad definition" % len(utility_broad_codes)) for code, cnt in util_districts_no_city.most_common(5): add("utility_district_%s" % names[code].lower().replace(" ", "_"), cnt, no_city_util, pct(cnt, no_city_util), "tax_dist %s" % code) add("mean_taxing_jurisdictions", "%.2f" % statistics.fmean(jur_counts), n_cre, "", "distinct codes in jurs per F1/F2 account") add("max_taxing_jurisdictions", max(jur_counts), "", "", "%d accounts at the maximum" % jur_hist[max(jur_counts)]) # finding 04 -- the layering does not thin out beyond the city line add("mean_taxing_jurisdictions_in_city", "%.2f" % statistics.fmean(jur_counts_city), in_city, "", "") add("mean_taxing_jurisdictions_no_city", "%.2f" % statistics.fmean(jur_counts_nocity), no_city, "", "") add("mean_taxing_jurisdictions_no_city_in_utility", "%.2f" % statistics.fmean(jur_counts_nocity_util), no_city_util, "", "outside every city and inside a utility district") ten_plus = sum(v for k, v in jur_hist.items() if k >= 10) add("commercial_taxed_by_10_or_more", ten_plus, n_cre, pct(ten_plus, n_cre), "") seven_plus = sum(v for k, v in jur_hist.items() if k >= 7) add("commercial_taxed_by_7_or_more", seven_plus, n_cre, pct(seven_plus, n_cre), "") # finding 02 -- address collisions add("distinct_commercial_site_addresses", len(addr_accts), "", "", "site_addr_1, uppercased and whitespace-collapsed, no other change") add("addresses_used_by_more_than_one", len(shared), len(addr_accts), pct(len(shared), len(addr_accts)), "") add("commercial_sharing_an_address", shared_accts, n_cre, pct(shared_accts, n_cre), "") add("commercial_address_no_house_number", placeholder_accts, n_cre, pct(placeholder_accts, n_cre), "site_addr_1 begins '0 ' or is '0'; HCAD's placeholder, not an address") add("addresses_used_by_more_than_one_excl_placeholder", len(shared_real), len(addr_accts), pct(len(shared_real), len(addr_accts)), "") add("commercial_sharing_an_address_excl_placeholder", shared_real_accts, n_cre, pct(shared_real_accts, n_cre), "") add("largest_address_group", largest, "", "", "accounts on one site_addr_1. VERIFIED FROM THE RAW ROWS, not from the " "string: all %d are one consecutive account-number block in one " "neighbourhood code on 0.13-0.17 acre lots, and 104 of them carry zero " "building area. It is a platted small-lot subdivision, not %d separate " "commercial buildings" % (largest, largest)) add("shared_addresses_spanning_more_than_one_key_map", len(dispersed_groups), len(shared), pct(len(dispersed_groups), len(shared)), "HCAD publishes NO COORDINATE in this extract, so study No. 03's " "distance test cannot be run. key_map, the Key Map grid page, is the " "finest locator available. WHAT IT CANNOT DISTINGUISH: a subdivision " "that straddles a grid line reads as dispersed, and the 112-account " "group below does exactly that. Treat this as an upper bound") add("commercial_sharing_address_across_key_maps", dispersed_accts, n_cre, pct(dispersed_accts, n_cre), "") add("commercial_sharing_address_across_key_maps_excl_placeholder", dispersed_real_accts, n_cre, pct(dispersed_real_accts, n_cre), "") add("commercial_with_no_key_map", no_keymap, n_cre, pct(no_keymap, n_cre), "") # finding 03 -- the absence of price add("recorded_conveyances_in_deeds_txt", deed_rows, "", "", "data rows, header excluded") add("deeds_txt_columns", len(deed_cols), "", "", " | ".join(deed_cols)) add("deeds_txt_price_columns", len(price_cols), len(deed_cols), pct(len(price_cols), len(deed_cols)), "columns whose name could carry a price, consideration, qualification " "code or instrument type. This is the finding") add("conveyances_with_a_date_and_no_price", deed_rows - deed_dos_blank, deed_rows, pct(deed_rows - deed_dos_blank, deed_rows), "dos populated; there is no amount column for any of them") add("commercial_conveyance_rows", deed_rows_cre, deed_rows, pct(deed_rows_cre, deed_rows), "deeds.txt rows on an F1/F2 account") add("commercial_accounts_with_a_conveyance", len(deed_accts_cre), n_cre, pct(len(deed_accts_cre), n_cre), "") add("commercial_conveyance_earliest_year", min(deed_years), "", "", "") add("commercial_conveyance_latest_year", max(deed_years), "", "", "") add("commercial_accounts_with_new_own_dt", new_own_dt_populated, n_cre, pct(new_own_dt_populated, n_cre), "real_acct.txt new_own_dt: another bare date, no companion amount") add("commercial_accounts_with_yr_annexed", yr_annexed_populated, n_cre, pct(yr_annexed_populated, n_cre), "the column exists on all 71 fields of real_acct.txt and is empty on " "every commercial account") # ---------------- print ---------------- print() print("HARRIS COUNTY, TEXAS -- COMMERCIAL RECORD, 2026 HCAD BUILD") print("Real_acct_owner.zip Last-Modified: %s" % acct_lm) print("Real_jur_exempt.zip Last-Modified: %s" % jur_lm) print("Universe: state_class F1 + F2. Aggregate counts only.") print() w = max(len(r[0]) for r in out) for metric, value, denom, share, note in out: line = "%-*s %14s" % (w, metric, "{:,}".format(value) if isinstance(value, int) else value) if denom != "": line += " / %-12s %6s" % ("{:,}".format(denom) if isinstance(denom, int) else denom, share) print(line) if note: print("%s %s" % (" " * w, note)) print() print("City codes used (OUR classification, block %03d-%03d less 069):" % (CITY_CODE_LO, CITY_CODE_HI)) for c in city_codes_in_table: print(" %s %-24s %s commercial accounts" % (c, names[c], "{:,}".format(city_accts[c]))) print() print("jurisdictions per commercial account:") for k in sorted(jur_hist): print(" %2d %s" % (k, "{:,}".format(jur_hist[k]))) if want_protest: protest_zip, protest_lm = fetch( HEARING_URL, paths[2] if len(paths) > 2 else None) heard = reduced = stood = 0 v0 = v1 = 0.0 hearing_accts = set() for r in read_rows(protest_zip, HEARINGS_MEMBER): if (r.get("Tax_Year") or "").strip() != "2026": continue if (r.get("State_Class_Code") or "").strip() not in CRE_CLASSES: continue a, b = num(r.get("Initial_Appraised_Value")), num( r.get("Final_Appraised_Value")) if not a: continue heard += 1 hearing_accts.add((r.get("acct") or "").strip()) v0 += a v1 += b if b < a: reduced += 1 else: stood += 1 print() print("NOT A PUBLISHED FINDING -- computed for the internal report only.") print("FLAVIO section 1: O'Connor & Associates holds this SERP with a") print("published '70-75%'. Do not fight for a term someone credible") print("already holds. Hearing_files.zip Last-Modified: %s" % protest_lm) print(" valued 2026 F1/F2 hearing rows %s" % "{:,}".format(heard)) print(" ... on distinct accounts %s" % "{:,}".format(len(hearing_accts))) print(" ended in a reduction %s %s" % ("{:,}".format(reduced), pct(reduced, heard))) print(" value stood %s %s" % ("{:,}".format(stood), pct(stood, heard))) print(" appraised value before/after $%sbn -> $%sbn (%s cut)" % (bn(v0), bn(v1), pct(v0 - v1, v0))) print(" commercial accounts protested %s %s of %s" % ("{:,}".format(protested_y), pct(protested_y, n_cre), "{:,}".format(n_cre))) if want_csv: header = [ "# Harris County, TX commercial property record: the jurisdiction count", "# Sources (all free, all a direct anonymous download -- no login, no fee,", "# no request form, no CAPTCHA, and no User-Agent workaround required):", "# Harris Central Appraisal District, 2026 CAMA public data", "# https://download.hcad.org/data/CAMA/2026/", "# Real_acct_owner.zip https://download.hcad.org/data/CAMA/2026/Real_acct_owner.zip", "# members real_acct.txt and deeds.txt", "# file last-modified %s" % acct_lm, "# Real_jur_exempt.zip https://download.hcad.org/data/CAMA/2026/Real_jur_exempt.zip", "# member jur_tax_dist_exempt_value_rate.txt", "# file last-modified %s" % jur_lm, "# The directory index at that path returns HTTP 500; the files return 200.", "# Downloaded and counted 2026-09-20.", "# Universe: %s accounts on the 2026 Harris roll; %s of them are commercial," % ("{:,}".format(len(all_accts)), "{:,}".format(n_cre)), "# meaning Texas state class F1 (commercial real) or F2 (industrial real).", "# Multi-family is excluded, as in field studies No. 01, No. 02 and No. 03.", "# The district classification is OURS, not HCAD's. HCAD publishes no district-type", "# column. Cities are the contiguous tax_dist code block 051-086 less 069 (NOT USED);", "# utility districts are a name match on MUD / WCID / UD / PUD. Print the full", "# code-to-class table with: python3 count_harris.py --districts", "# 'No city' is NOT 'in the ETJ'. jurs records taxing jurisdictions and an", "# extraterritorial jurisdiction levies no tax, so it cannot appear in the column.", "# No figure below is an ETJ count.", "# HCAD publishes no certified or dated snapshot, and its prior-year directories are", "# rebuilt too: on 2026-09-20 the 2025 and 2024 files carried the same", "# 2026-09-13 Last-Modified as the 2026 files. A LATER FILE WILL NOT REPRODUCE", "# THESE FIGURES EXACTLY. What reproduces is the method.", "# Texas is a non-disclosure state. No figure below is a sale price, because the", "# record contains none -- that absence is finding 03 and it is never expressed", "# as a share of hidden sales, since the denominator is what non-disclosure hides.", "# Aggregate counts only. No owner, mailing, grantor, grantee or legal-description", "# data is read or reproduced. owners.txt is never opened.", "# Recompute with count_harris.py, published beside this file.", "# By Chris Klebl. Published by Sterling Digital Partners.", "# Free to reuse with attribution and a link.", ] with open(want_csv, "w", newline="") as fh: for line in header: fh.write(line + "\n") wcsv = csv.writer(fh) wcsv.writerow(["metric", "value", "denominator", "share", "note"]) for row in out: wcsv.writerow(row) sys.stderr.write("wrote %s\n" % want_csv) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))