< back to series
DAY 5

County GIS Data, SQLite, and Ownership Classification

Mar 22, 2026

The agent was offline this day due to model credit limits, so this portion was done directly with Claude.

The problem

A striping business sells to whoever controls the lot, usually the property owner. The CRM had business names and addresses but no ownership data. Without that field, owner-operators, tenants, and property managers were mixed together.

The data source

Collin County parcel data from TNRIS (Texas Natural Resources Information System), downloaded from the StratMap Land Parcels collection. The same dataset was later expanded to Dallas, Denton, and Tarrant counties. The export includes:

ColumnDescription
prop_idUnique parcel identifier
situs_addrProperty street address
owner_nameName on the deed
land_useCommercial, retail, church, etc.
acresLot size
market_valueCounty assessed value
year_builtYear of construction

SQLite table

sqlCREATE TABLE parcels (
    prop_id      INTEGER PRIMARY KEY,
    situs_addr   TEXT,
    owner_name   TEXT,
    land_use     TEXT,
    acres        REAL,
    market_value INTEGER,
    year_built   INTEGER,
    city         TEXT
);

CREATE INDEX idx_parcels_addr ON parcels (situs_addr);
CREATE INDEX idx_parcels_city ON parcels (city);

Matching prospects to parcels

Each CRM prospect gets looked up by street address with fuzzy matching:

pythondef parcel_lookup(street: str, city: str, db_path: str) -> dict | None:
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()
    street_norm = normalize_street(street)
    cur.execute("""
        SELECT prop_id, owner_name, situs_addr, acres, market_value, year_built, land_use
        FROM parcels
        WHERE situs_addr LIKE ? AND city = ?
        LIMIT 1
    """, (f"%${street_norm}%", city.upper()))
    row = cur.fetchone()
    conn.close()
    if not row:
        return None
    keys = ["prop_id", "owner_name", "situs_addr", "acres", "market_value", "year_built", "land_use"]
    return dict(zip(keys, row))

Ownership classification

Comparing parcel owner name to business name:

  • owner_operator: names match. Direct sale, highest priority.
  • tenant: parcel exists, different owner. Wrong contact.
  • property_manager: no match or known PM company. Different pitch needed.

Token overlap matching handles formatting differences:

pythondef names_match(owner_name: str, business_name: str) -> bool:
    def tokens(s):
        s = s.upper()
        for stop in ("LLC", "INC", "LP", "THE", "OF", "AND", "&", "CO", "CORP"):
            s = s.replace(stop, "")
        return set(s.split())

    ow = tokens(owner_name)
    biz = tokens(business_name)
    if not ow or not biz:
        return False
    overlap = ow & biz
    return len(overlap) / max(len(ow), len(biz)) >= 0.6

Result

Owner-operators rose to the top of the call queue:

statuslead_typemeaning
call_nextowner_operatorBusiness owns the lot, call first
newtenantDoesn't control the lot
newproperty_managerUnconfirmed, research first

Later, the parcel table moved from local SQLite to D1 behind a Worker endpoint at GET /api/parcels/search.