DEV Community

Loomwise
Loomwise

Posted on

Find the Busiest Contractors in Any City from Building Permit Data (A B2B Lead List in Python)

If you sell to contractors (materials, equipment rental, insurance, software, financing, staffing), your best prospects are the ones pulling the most permits right now. Permit counts are public. Many cities publish every permit with the contractor's business name and the declared project value, so you can rank every contractor in a market by real activity instead of guessing from Google reviews.

This post builds a "busiest contractors" ranking from 90 days of permits in three markets, with one Python script, and shows how to clean the names so the same company groups together.

Run on Oct 22, 2026 (last 90 days):

  • Miami-Dade County, FL: 17,170 permits from 4,012 contractor businesses. The busiest were American Residential Services of Florida (172 permits), Lennar Homes (148 permits, $31.6M declared) and Arnold J Electric (110 permits, $12.4M).
  • Orlando, FL: 5,937 permits from 1,467 businesses. Pulte Home Company led with 230 permits and $30.8M, followed by Josko LLC (136) and Wayne Automatic Fire Sprinklers (100).
  • Everett, WA: 1,979 permits from 575 businesses. CM Heating led with 88, then VECA Electric & Tech (50) and Sequoyah Electric (42).

The data sources

Market Platform Dataset Contractor column
Miami-Dade County ArcGIS REST miamidade_permit_data ContractorName
Orlando Socrata ryhf-m453 contractor_name
Everett Socrata 3w3u-656c contractorname

The script

import re, requests
from collections import defaultdict
from datetime import date, timedelta
since = date.today() - timedelta(days=90)
ENTITY = re.compile(r"\b(LLC|L\.L\.C|INC|CORP|CORPORATION|CO|COMPANY|LTD|LLP|LP|PLLC|ENTERPRISES|SERVICES|CONSTRUCTION|ROOFING|ELECTRIC|PLUMBING|HEATING)\b", re.I)

def clean(name):
    """Normalize a contractor name so the same company groups together."""
    n = re.sub(r"\((CONST|ELECT?|PLMB|MECH)\)", "", name.upper())      # Everett appends the license type
    n = re.sub(r"[.,\\]", "", n)
    return re.sub(r"\s+", " ", n).strip()

def socrata(domain, ds, dfield, cfield, vfield):
    rows = requests.get(f"https://{domain}/resource/{ds}.json", params={
        "$select": f"{cfield} AS c, {vfield} AS v", "$where": f"{dfield} >= '{since}' AND {cfield} IS NOT NULL",
        "$limit": 50000}, timeout=120).json()
    return [(r["c"], r.get("v")) for r in rows]

def miami_dade():
    url = "https://services.arcgis.com/8Pc9XBTAsYuxx9Ny/arcgis/rest/services/miamidade_permit_data/FeatureServer/0/query"
    out, offset = [], 0
    while True:
        js = requests.get(url, params={"where": f"PermitIssuedDate >= DATE '{since}' AND ContractorName IS NOT NULL",
              "outFields": "ContractorName,EstimatedValue", "resultOffset": offset, "resultRecordCount": 2000, "f": "json"}, timeout=120).json()
        feats = js.get("features", [])
        out += [(f["attributes"]["ContractorName"], f["attributes"]["EstimatedValue"]) for f in feats]
        if not js.get("exceededTransferLimit"): return out
        offset += len(feats)

def leaderboard(pairs, top=5):
    agg = defaultdict(lambda: [0, 0.0])
    for name, val in pairs:
        n = clean(name or "")
        if not ENTITY.search(n) or n in ("OWNER", "OWNER BUILDER"):
            continue  # keep businesses only; skip owner-builders and personal names
        agg[n][0] += 1
        try: agg[n][1] += float(val or 0)
        except ValueError: pass
    return sorted(agg.items(), key=lambda kv: kv[1][0], reverse=True)[:top], len(agg)

for label, pairs in [
    ("Everett, WA", socrata("data.everettwa.gov", "3w3u-656c", "issueddate", "contractorname", "jobvalue")),
    ("Orlando, FL", socrata("data.cityoforlando.net", "ryhf-m453", "issue_permit_date", "contractor_name", "estimated_cost")),
    ("Miami-Dade County, FL", miami_dade()),
]:
    top, n = leaderboard(pairs)
    print(f"{label}: {len(pairs)} permits, {n} contractor businesses. Busiest:")
    for name, (cnt, val) in top:
        print(f"   {cnt:4d} permits  ${val:>13,.0f}  {name}")
Enter fullscreen mode Exit fullscreen mode

Three things that will trip you up

  1. One company, many spellings. Everett appends the license type, as in "VECA (ELECT) ELECTRIC & TECH LLC", and Miami-Dade escapes commas with a backslash. clean() strips these so permits group under one name. Look at the top 50 names in each new city and add rules as needed.
  2. Skip owner-builders and people. Permits pulled by homeowners say "OWNER", and some list a person's name. The script keeps only names with a business marker (LLC, Inc, Construction, Roofing...). That keeps the list B2B and avoids personal data.
  3. Count and dollars tell different stories. Service companies (HVAC, electrical, fire sprinklers) pull many small permits, and home builders pull fewer, bigger ones. Rank by count to find service businesses and by declared value for builders, depending on who you sell to.

Turning it into a sales list

  • Export to CSV and add the trade (from the name or permit type) and a rank.
  • Re-run monthly and watch movers: a contractor whose permit count doubled is hiring and buying.
  • Add more markets: any Socrata or ArcGIS permit feed with a contractor column works. Just add a line to the list at the bottom.

Optional: I run a hosted permits tool covering 47 cities and counties in 20 states plus DC, with contractor names, values and addresses in one format: US City Building Permits (paid per result). Disclosure: it's my tool. The script above does the same job for free for the markets you add.

Top comments (0)