Phoenix Permits scrapes and parses the City of Phoenix's public Planning & Development permit records (PDD), enriches them with geocoding and derived risk signals, and exposes the result through a set of pages built around one shared query language.
| Page | What it's for |
|---|---|
| /search | Filterable permit table + embedded map, split-pane. Best for browsing/exploring visually. |
| /analysis | "Trouble Sleeping?" — EXTH/EXTR overnight-construction permits within a set distance of residential parcels, with duration/night-hours highlighting. |
| /query | The full query language (this doc). Rows, bar charts, and a risk-colored map view. |
| /chains | Groups permits by project/address into renewal chains (e.g. BLD → EXTH → EXTR renewals). |
| /contractors | Contractor-level rollups: permit counts, valuation totals, approver relationships. |
| /patterns | Cross-permit statistical patterns (approver concentration, timing anomalies). |
| /income | City permit-fee revenue rollups ("City Income"). |
| /sql-search | Scheduled/saved searches — any /query search can be saved and run on a recurring schedule, optionally emailing results. |
The /query bar speaks a Sumo Logic-flavored language: a filter expression, optionally followed by one or more | pipe stages. Nothing here ever touches raw SQL — every field is resolved through a fixed whitelist and every value is parameterized.
Unquoted values are case-insensitive; quoting a value makes it case-sensitive. A bare word with no field name (or a quoted phrase) runs a trigram search across address, description, scope of work, type number, owner, and superintendent.
| Alias | Column | Type |
|---|---|---|
| type / permit_type | permit_type | text |
| type_number | type_number | text |
| status | status | text |
| address | address | text |
| zip | zip | text |
| district / dist | dist | text (source-reported — see caveat below) |
| apn | apn | text |
| zoning | zoning | text |
| project / project_id | project_id | text |
| approver / issued_by | issued_by | text |
| entered_by | entered_by | text |
| superintendent | superintendent_name | text (EXTH/EXTR only) |
| owner | owner_name | text |
| description | permit_description | text |
| work | description_of_work | text |
| work_days / work_hours | work_days / work_hours | text (EXTH/EXTR only) |
| issued / issued_date | issued_date | date |
| expires / expires_date | expires_date | date |
| work_from / work_to | work_date_from / work_date_to | date |
| landed / created_at | created_at | timestamp — when we scraped it |
| valuation | valuation | numeric |
| sq_ft | sq_ft | numeric |
| lat / lng | lat / lng | numeric |
| contractor | contractors.name | text (auto-joins) |
| license | contractors.license | text (auto-joins) |
| result / inspector / inspection_type / discipline | inspections.* | text (auto-joins) |
| scheduled_date / completed_date | inspections.* | date (auto-joins) |
Caveat on district: this is the raw value from the source PDD listing and has real data-quality gaps (some rows contain OCR/parsing artifacts instead of a real 1–8 district number). For reliable district assignment, prefer geometry — join through council_districts via ST_Contains, the same way mv_exth_near_residential does — rather than trusting this field directly for anything district-ranking related.
One or more comma-separated aggregate calls, sharing one by group-field list — matches Sumo's real multi-function grammar. Function-call form and the legacy bare shorthand both work:
Available functions: count, count_distinct, sum, avg, min, max, first, last. sum/avg require a numeric or boolean field (boolean is cast to 0/1). Aggregate results render as Rows or a vertical Bar chart with a click-to-exclude legend below it that rescales the chart.
Buckets issued_date into fixed windows, must be paired with an aggregate that groups by it:
Fixed-duration units (s/m/h/d/w) can be subdivided into equal parts; calendar units (mo/y) can't, since months/years vary in length.
Computed for EXTH / EXTR (extended construction hours) permits by the refresh_exth_signals recurring job. All are queryable and all show up in the map view's marker color and popup.
| Field | Meaning |
|---|---|
| self_approved | The same person both entered and approved (issued_by = entered_by). |
| flagged_approver | Approved by an identity on the flagged-approver list. |
| backdated_days | Days between the permit's stated effective/start date and its recorded issue date — a positive value means it was issued after work was already supposed to have started. |
| seven_day_coverage | The permitted work-days field covers all seven days of the week. |
| new_address | First EXTH/EXTR permit ever recorded at this address. |
| renewal_count | How many prior permits in the same renewal chain (same address + description) came before this one. |
| near_res_ft | Distance in feet to the single nearest residential parcel (uncapped — the /analysis page's own view caps this at the 500ft statutory line; querying the field directly does not). |
| risk_score | Weighted sum of the above (new_address 30, renewal_count≥3 20, seven_day_coverage 15, near_res_ft<500 15, self_approved 10, flagged_approver 10, backdated_days>0 25). Not a legal conclusion — a triage signal for which permits are worth reading in full. |
Note on near_res_ft coverage: the underlying residential-parcel dataset is a targeted import (~6k parcels near known permits), not a full citywide layer. Distances under roughly 1,000–1,500ft are reliable; larger values may just reflect a gap in parcel coverage near that permit rather than a true distance.
On /query, any row-level (non-aggregate) result with lat/lng exposes a Map view alongside Rows. Each permit renders as a marker colored by risk_score (green <20, yellow 20–49, orange 50–74, red ≥75, gray if no score). Click a marker for a popup with the key fields and a "View details" link into the full permit modal.