landrecords-card-reader
Extract structured property data from assessment card PDFs using LLM-powered text extraction.
Property cards (also called land cards or assessment cards) are PDF documents produced by county tax assessors.
Installation
pip install landrecords-card-reader
With optional extras:
# LangGraph wiring (only needed if you want to use the graph node)
pip install landrecords-card-reader[graph]
# Everything
pip install landrecords-card-reader[all]
System dependencies
- Ollama running locally or on a remote host with a text model loaded
(e.g.
gemma4:26b-a4b-it-q8_0) - wkhtmltopdf for HTML→PDF conversion of property pages that aren't
served as PDFs:
# macOS brew install --cask wkhtmltopdf # Debian/Ubuntu sudo apt-get install wkhtmltopdf
OCR (docTR + PyTorch) ships as a Python dependency; the model weights
(~100MB) are downloaded to ~/.cache/doctr/ on first call.
Quick start
from landrecords_card_reader import read_property_card
data, photo = read_property_card("https://example.com/card.pdf")
print(data["ownername"]) # "SMITH, JOHN A"
print(data["totalvalue"]) # 285000
print(data["parceladdr"]) # "123 MAIN ST"
# photo is raw image bytes of the first property photo, or None
if photo:
with open("photo.jpg", "wb") as f:
f.write(photo)
Use analyze_photo=True to send the property photo (if it exists) to the
vision model, filling in missing building details (exterior walls, roof
style, number of floors, etc.):
data, photo = read_property_card(url, analyze_photo=True)
If you already have the PDF bytes, pass them directly to skip the download:
data, photo = read_property_card(url, pdf_bytes=raw_bytes)
For URLs that might be HTML property report pages (e.g. Beacon, Tyler,
or other county assessment sites), use read_property_card_from_url.
It fetches the URL, detects whether the response is a PDF or HTML, and
converts HTML pages to PDF via pdfkit (wkhtmltopdf) automatically:
from landrecords_card_reader import read_property_card_from_url
data, photo = read_property_card_from_url(
"https://www.webgis.net/LinkedFiles/va/pulaski/pc/cards/PC17759.htm"
)
CLI
landrecords-card-reader https://example.com/card.pdf --dry-run -v
Configuration
Set via environment variables or a .env file:
| Variable | Default | Description |
|---|---|---|
CARD_READER_LLM_BASE_URL |
http://graybase:8000/v1 |
OpenAI-compatible chat-completions endpoint |
CARD_READER_LLM_API_KEY |
dummy |
API key sent as Authorization: Bearer … (use any non-empty string for self-hosted servers) |
CARD_READER_EXTRACTION_MODEL |
gemma4:26b-a4b-it-q8_0 |
Model for structured extraction |
CARD_READER_PHOTO_CLASSIFICATION_MODEL |
gemma4:e2b |
Lightweight vision model for photo classification |
Extracted fields
The extraction prompt maps over 80 property card fields including:
- Identity: parcelid, taxacctnum, taxyear
- Owner: ownername, owneraddr, ownercity, ownerstate, ownerzip
- Location: parceladdr, parcelcity, parcelstate, parcelzip, legaldesc
- Valuation: landvalue, imprvalue, totalvalue, assessedvalue, appraisedvalue
- Building: yearbuilt, bldgsqft, bedrooms, fullbaths, halfbaths, bldgtype
- Construction: foundation, roofcover, extwall, heating, heatfuel, cooling
- Sale: saleamt, saledate
- Zoning: zoningcode, zoningdesc, zoningtype
How it works
- Download the PDF (or accept pre-downloaded bytes)
- In parallel:
- OCR every page via docTR — PDF bytes are passed straight to
DocumentFile.from_pdf(rasterised internally via pypdfium2) and run through docTR's deep-learning detection + recognition models in a single batched inference call. The predictor usesassume_straight_pages=Trueand runs on CUDA when available - Extract & classify property photos — candidate images are filtered by size/aspect ratio, then sent to a vision model to keep only actual photographs (discarding sketches, floorplans, maps, etc.)
- OCR every page via docTR — PDF bytes are passed straight to
- Extract structured data by sending the raw OCR text to an Ollama LLM
- Reconcile values — verifies
landvalue + imprvalue == totalvalueand computes any single missing value arithmetically - Targeted retries — if
parcelidis too short, re-asks; if a heat-fuel label is present butheatfuelis empty, runs a deterministic regex fallback then a focused LLM retry; if any registered field's label is in the OCR text but the value is empty, batches them into one LLM retry
License
MIT
Metadata
Release files for landrecords-card-reader 0.7.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| landrecords_card_reader-0.7.2.tar.gz | 73.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| landrecords_card_reader-0.7.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 167.2 kB
Release files / landrecords_card_reader-0.7.2.tar.gz
| Download URL | landrecords_card_reader-0.7.2.tar.gz |
|---|---|
| Size | 73.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3a69b9b6cff66758592b728dd711a19d3ad86b8b25aa68faf090605a1660254a
|
|
BLAKE2b-256 checksum How to use checksums |
6f1532a8198fd68509137fa33100cc986d7df8e94425ce4d35b8fedac28b8937
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Release files / landrecords_card_reader-0.7.2-py3-none-any.whl
| Download URL | landrecords_card_reader-0.7.2-py3-none-any.whl |
|---|---|
| Size | 93.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b1749e2347c651efbc97445a3c0863009506080a88234baab4ee56b772898301
|
|
BLAKE2b-256 checksum How to use checksums |
4c07fdd1b104da528c594761aa1fc686edca01e4f852927e8704c7ea110d39d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|