vccs
Composable charting components for Vue 3, ported from Recharts
Live parcel search for Manitoba Open Data — Roll Entry + Zoning + Development Plan + Traffic + Contaminated Sites, queried directly from the province's ArcGIS REST endpoints.
DataJavaScript · RGitHub @jayschellenbergOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Loading the full README. The saved excerpt is shown below.
A firm-facing web tool for researching Manitoba properties (excluding Winnipeg, which has its own portal) by municipality + civic address + roll # + legal description, with each result enriched from provincial open-data sources and rendered on an interactive map.Read the full README ↗A firm-facing web tool for researching Manitoba properties (excluding Winnipeg, which has its own portal) by municipality + civic address + roll # + legal description, with each result enriched from provincial open-data sources and rendered on an interactive map. Pulls together what would otherwise be a multi-tab workflow — Manitoba Assessment Online, the provincial Zoning By-Laws layer, the Development Plan Designations layer, the MHTIS traffic-flow layer, MASC soil ratings, Water Rights Licensing (tile drainage + irrigation), and the Contaminated Sites Registry — into one search window with a single CSV export.
Source: https://github.com/jayschellenberg/manitoba-opendata-parcelsearch. Deployed on Vercel from main.
| Path | Purpose | Audience |
|---|---|---|
web/ | Vite + vanilla JS static site. Queries Manitoba Open Data's ArcGIS REST API live on every search. Deployed to Vercel. | Firm colleagues |
r/download_parcels.R | Snapshots the three primary FeatureServer layers to dated GeoPackages. | Local archive |
r/parcel_search_app.R | Shiny app that searches the local snapshot for offline / historical lookups. | Personal use |
r/build_legal_index.R | Converts ../mao-scrape/results/parcels.parquet into the static browser legal-search index at web/public/data/legal-index.json. | Web deploy prep |
r/build_masc_shards.R | Splits the MASC quarter-section rating CSV into per-municipality static shards for the MASC Rating map layer. | Web deploy prep |
r/build_parcel_masc.R | Pre-bakes the dominant MASC soil rating per Roll Entry parcel for the Soil table column and writes the rated river-lot MASC overlay when river-lot inputs are present. | Web deploy prep |
r/build_mli_ortho.ps1 | Downloads and builds the full southern-Manitoba MLI historical aerial mosaic as local PMTiles. | Basemap build |
vercel.json | Build config + production CORS rewrite for the contaminated-sites CSV. | — |
HANDOFF-WATER-INFLUENCE.md | Water-influence feature notes: repo map, algorithm history, UI decisions, verification steps, and open follow-ups. | Maintainers |
REPLICATION_GUIDE.md | Step-by-step guide for adapting this tool to another jurisdiction. Originally written for the Winnipeg sister site (Socrata); §14 captures every Manitoba-specific decision and lesson. | Anyone replicating |
Most parcel, zoning, dev-plan, traffic, and environmental data is queried live. Search results are always against the current state of the province's published data; auxiliary overlay datasets are cached in the browser for 7 days (clearable from the Clear button) so repeat work in the same area stays snappy.
Legal-description search is backed by a generated static index from the companion MAO scrape. Run this after ../mao-scrape/results/parcels.parquet is assembled or refreshed:
cd web
npm run legal:index
The browser searches that index for legal description text, Lot/Block/Plan, certificate-of-title text, and the structured MAO-parity criteria (condo plan/unit, parish lot, section-township-range) matched against tokens derived from the legal text on first use. Matching (muni_no, roll_no_txt) keys are used only as a lookup bridge; the app still fetches the current parcel geometry and assessment fields live from Roll Entry.
MASC soil data is also shipped as generated static artifacts rather than queried live. The quarter-section layer uses per-municipality JSON shards under web/public/data/masc/, rated long/narrow river-lot polygons are in web/public/data/masc-riverlots.json, and the table's dominant parcel-level soil rating uses web/public/data/parcel-masc/. Risk-area numbers come from the official Manitoba Maps / Open Canada MASC_Risk_Areas polygon layer and are joined to parcel results at search time. Refresh the generated soil artifacts after a new MASC CSV, river-lot scrape, river-lot KMZ, or Roll Entry snapshot:
Rscript r/build_masc_shards.R
Rscript r/build_parcel_masc.R
| Dataset | FeatureServer | Used for |
|---|---|---|
| ROLL ENTRY | …/ROLL_ENTRY/FeatureServer/0 | Parcel polygons, roll #, civic address, municipality, dwelling units, total assessed value, link to MAO report |
| Manitoba Zoning By-Laws | …/Manitoba_Zoning_By_Laws/FeatureServer/0 | Zone code, zone name, category, governing zoning bylaw (ZBL), amendment bylaw (ZBL_A), amendment description |
| Development Plan Designations | …/Manitoba_Development_Plan_Designations/FeatureServer/0 | Designation name, category, dev-plan bylaw (DP_BYLAW), amendment bylaw (DPA_BYLAW), planning district |
| MHTIS Traffic Flow 2025 | …/E_MHTIS_LRS_20260923/FeatureServer/1 | AADT polylines for the colour-coded Traffic overlay. AADT + per-segment AADT_YEAR; field names are mapped to the app's at fetch (TRAFFIC_FLOW_FIELDS in arcgis.js). Replaced the withdrawn MHTIS_Traffic_Flow_2023_(new) on 2026-09-23 |
| Manitoba Road Network 2023 | …/Manitoba_Road_Network_2023/FeatureServer/0 | Province-wide PTH, PR, access, service, ramp, and winter-road overlay |
| Manitoba Contaminated Sites Registry | CSV at manitoba.ca/.../cs-data.csv (proxied via vercel.json because the upstream lacks Access-Control-Allow-Origin) | Designated Contaminated, Designated Impacted, and Not Designated sites for the Enviro overlay |
| MAO scrape legal index | web/public/data/legal-index.json generated from |
Nine zones, grouped into five map toggles and one table column. The column says whether a parcel is in a flood zone, which one (strongest first, ranked by legal force), and what share of the parcel is inside it — a quarter section 4% inside a Designated Flood Area is not encumbered the way a lot wholly inside it is.
The overlay and the column are decided from different geometry on purpose:
the map draws MBFloodMapping's web-simplified layers (3–30% of the vertices,
right for a provincial boundary at town zoom), while the column joins the
full-resolution cache in r/build_flood.R. A boundary parcel can therefore
look outside the DFA on screen and read RRV DFA 12% in its cell.
The Flood cell keeps its ↗ deep-link into the standalone flood-screening tool, which still does what a set-based grid cannot: the appraisal paragraph, the static exhibit, the NRCan study-coverage index, and the full source disclosure.
Rebuild both after MBFloodMapping refreshes (see MAINTENANCE.md §6c):
cd web
npm run flood:overlay # web/public/data/flood/ — commit here
npm run flood:shards # mb-parcel-data/flood/ — publish per §1b
Not a flood determination. These are screening layers. The two statutory Designated Flood Area boundaries and the Special Management Area come from the Manitoba Land Initiative, which stopped publishing updates on 2022-02-09; a newer DFA layer exists on DataMB and has deliberately not been repointed to. Verify any boundary against MTI Designated Flood Areas before relying on it.
Manitoba has no province-wide civic-address dataset, so unlike the
Winnipeg sister site —
which recovers a parcel's alias addresses by joining a separate address-point
layer to the parcel polygon — there is no second source to join here. Every
address the app knows is the one string in ROLL_ENTRY.Property_Address.
The aliases are inside that string instead, and two forms of it name addresses the parcel answers to but does not begin with:
| Form | Rows | Example |
|---|---|---|
| Civic range | 8,964 | 1511 - 1519 26TH ST (Brandon roll 510875) |
| Unit ahead of the building address | 5,972 | Unit 10 - 1015 26TH ST (roll 525063) |
Keyed to its leading token, the first was findable only as 1511 or 1519 — 1515
26th St, a real address on that parcel, returned nothing — and the second keyed
to no civic number at all, so an exact or range search silently dropped every
condo unit in the province. parseCivicAddressSpans in lib/civicRange.js now
reads an address as an inclusive span rather than a point, and the filter
tests interval overlap, so an interior number matches and a unit address keys
to its building.
The spaced hyphen is the whole discriminator and must not be relaxed: every
unspaced hyphen in this column is a legal description, not a range —
15-42-244 is lot 15 of plan 42, NW6-6-29W a quarter section. A looser regex
would read thousands of those as civic ranges.
One matching change in arcgis.js: an exact search narrows server-side with an
anchored prefix (LIKE '1515%'), which a range row can never satisfy, so range
rows on the searched street are OR'd in for the client filter to decide. That
OR rides only on a query already bounded by street name or municipality — bare
province-wide it would add ~9k rows and crowd the MAX_RESULTS cap.
The page splits into a fixed-width left sidebar holding all controls and a fluid main pane holding the map and table. On viewports below 900 px the layout collapses to a single column.
Sidebar — Search section:
Municipality dropdown (preloaded with every distinct Muni_Name_With_Typ value; narrows the Zoning Category dropdown to the codes actually present in the muni)
Civic Address (case-insensitive LIKE)
Roll # — single value or a list. One roll runs as before; pasting many runs them all in one query and the count badge calls out any rolls that didn't match (e.g. 23 of 25 rolls matched · 2 of 25 not found: 1234, 5678). Accepts both 3600 and 3600.000. Cross-muni bulk via LINC is documented as future work in REPLICATION_GUIDE §15.5.
One punctuation rule: everything separates. Comma, space, newline, ;, &, + and | all mean "these are different properties", and spacing is irrelevant — 284950&373300, 284950 & 373300 and 284950, 373300 are the same search. Each typed roll becomes its own chip, its own map highlight and badge number, and its own Parcel Snapshot.
Earlier builds treated
+,&and|as joiners — the rolls either side merged into one subject sharing a single badge and one combined snapshot image. That merge is gone. Property Search never groups parcels at all: not from Roll # punctuation, and not from a parcel-list import row that lists several rolls in one cell (those expand to one independent parcel each). Grouping is a sales concept — the combined $/acre across a multi-parcel transaction — so it lives on the Sales tab and comes only from an uploaded sales CSV.
Street Type (AVE, ST, DR, …) and Direction (N, S, E, …) dropdowns under the address row, mirroring MAO's civic-address search. Both are decided client-side against the Property_Address text: the type must sit in type position ('ST' finds "100 MAIN ST N" but not "123 ST MARYS RD", and long forms match — RD also finds "ROAD"), the direction as its own word or glued to a rural grid number ("ROAD 96W")
Legal Description (contains), Lot / Block / Plan (exact), and Certificate of Title (contains) from the generated MAO scrape index
MAO-parity structured searches (Advanced group), matched against tokens derived from the scrape's legal text at index load (legalIndex.core.js):
plan-unit pairs condominium titles carry (38239-1 TOGETHER WITH …); unit ranges (DESC 1/8-39000) count as every unit they coverSidebar — Map overlays section. Each category is a collapsible
<details> group, with the toggles laid out two-up inside an inner
.overlay-group-body grid. The grid has to live on that wrapper: a
<details> renders its non-summary content inside an anonymous box, so a
grid declared on the <details> itself would treat that whole block as a
single grid item and stack the buttons in one column.
Six groups: Parcel layers, Historical, Planning, Reference, Agricultural and Quick links. Reference, Agricultural and Historical ship collapsed; the rest open. Open/closed state is not persisted — what you expand stays expanded for the life of the page (the panel is never re-rendered, so it survives tab switches and searches on its own) and a reload returns to those defaults. Persisting it was worse in practice: opening Agricultural once to reach a layer meant it stayed open on every future visit, so "collapsed by default" quietly stopped being true.
A collapsed group with active settings shows an "N on" badge tinted to
that group's colour — counting ticked search filters as well as pressed
layer toggles, since a filter silently narrowing every search is exactly
what must not go invisible. Collapsed contents are display: none, so
they're out of layout and out of the tab order.
The Historical group pairs its Show button with the As of date picker on one line, so the date reads as belonging to the toggle. The two water-rights search filters sit at the foot of Agricultural, beside the overlays they filter on. Because the Map layers panel renders under whichever tab is active, that single pair of checkboxes serves both Property Search and Sales Analysis with no duplicated control to keep in sync — and they work on an imported sales CSV too, narrowing the comps to tiled or irrigated parcels, with the count line naming how many it hid (e.g. 5 of 5 sales plotted · 2 hidden by the licensed tile drainage filter).
Their two grid columns read as a scannable yes/no — Tile Drainage as
Yes · 88% or No record, Irrigation as Yes or No record — with the
licence number, licensee, status, legal location and specs on hover.
The Irrigation column, its CSV columns, and its search filter all key on licensed points of use only. A point of diversion is an intake or a well: water is taken from there, not applied to it, so it says nothing about whether a parcel is irrigated. Diversions still draw on the map overlay (blue, against violet for points of use); they are simply not reported per parcel. Excluding them also halves the filter's work — about 2,500 points of use province-wide rather than 5,300 footprints across both kinds — which is why an irrigation-filtered RM of Portage la Prairie search now returns a complete 691 parcels instead of truncating at the 1,000-row cap.
Only Tile Drainage carries a percentage, and that is deliberate. The two layers are different kinds of geometry:
The CSV carries Irrigation Location (the survey quarter the licence
names) for evidence work. No record is not No: WALLAS holds
licensed works only and its tile polygons lag, so the honest claim is
"Manitoba has no licensed record here", not "this land is undrained".
Blank means the overlay hasn't been switched on yet, which is a third
state again. Both columns are .water-only, so they reveal themselves
whenever a WALLAS overlay or search filter is active and hide again when
it isn't — the same mode-class pattern the Dev-Plan columns use.
That applies in sales-analysis mode and parcel-list imports too: both
run the same enrichment, so an imported sales CSV gets tiled/irrigated
status per comp, and the CSV export leads each group with a filterable
Tiled / Irrigated column.
Tile * CSV columns. Every query filters APPLICATION_STATUS to the four licensed values, so rejected and abandoned applications never render.USAGE_CATEGORY = 'Irrigation'. Popups carry licensee, groundwater-vs-surface source, works type, and aquifer or water-source name. Also populates the Irrigation table column — a plain Yes / No record — plus the five Irrigation * CSV columns (led by a filterable Irrigated yes/no). Only licensed points of use are reported per parcel; points of diversion are intakes and wells, so they draw on the map but say nothing about whether land is irrigated.MUNI_WEBSITES in main.js). Reads "Muni N/A" when there is no entry for that muni.PLANNINGDISTRICT value across the dev-plan enrichment FC picks the active PD; PD_WEBSITES looks up its URL. Reads "PD N/A" when the PD has no website on file. Stays disabled until a search resolves the PD.Basemap menu sits in the map's top-right gutter and offers Protomaps Streets,
Esri Satellite, the Natural Resources Canada transportation map, and the NRCan
elevation hillshade. A fifth
MLI aerial 2007-2013 row appears when the locally built historical PMTiles
archive is hosted and VITE_MLI_ORTHO_PMTILES_URL is set. While it is active,
the trigger reports the acquisition year at the map centre. See
docs/MLI-IMAGERY-BASEMAP.md.
| Roll # | Address | Legal | Title | Zoning | % | Zoning 2 | ZBL | Dev-Plan Designation | DP By-law | Soil | Risk Area | Changes | DU | Acres | SF | Assess-{year} | Walkscore | Flood |
RR-RL-F and MA-RL-94B; the cell tooltip identifies the source quarter or river lot. Risk Area is stamped from the official MASC_Risk_Areas polygon containing the parcel's representative point. Parcels outside the relevant coverage render blank.Z: AG-5 → RR1 or DP: 03/10 → 23-05 when an amendment is recorded for the row's primary overlay match. Whitespace and Esri <Null> sentinels in the source data are filtered out so spurious "Z: " entries no longer appear.Asmt_Roll across the result set. The dollar value itself is the link to the parcel's MAO report.walkscore.com/score/<address>; no API key needed (the Walk Score page renders Walk / Transit / Bike on arrival).?lat=<centroid>&lon=<centroid>&label=<address>. Falls back to ?address=… when geometry is missing.All columns sortable; CSV export carries the same column order plus legal detail, parsed Lot / Block / Plan, certificate-of-title text, raw coverage ratios, and the URLs for Walkscore / Flood / MAO Report.
The MASC Rating layer only paints ratings that exist in the generated quarter-section shards or the rated river-lot overlay. A 2026-05-07 coverage audit found zero MASC source centroids inside the largest blank polygons for WEST INTERLAKE (RM), PINEY (RM), TACHE (RM), RIDING MOUNTAIN WEST (RM), DAUPHIN (RM), ALONSA (RM), GILBERT PLAINS (MUNICIPALITY), PORTAGE LA PRAIRIE (RM), TWO BORDERS (MUNICIPALITY), and ARMSTRONG (RM), even when checking for ratings filed under another municipality. Treat those blanks as source coverage gaps or non-rated areas unless a newer MASC source file adds ratings there; do not fill them by nearest-neighbour interpolation for the map overlay.
A Generate Static Map button between the table and the About section captures the current MapLibre view (extent, zoom, basemap, every active overlay) as a PNG. The captured image has the basemap attribution composited into the bottom-right corner so it travels with the file when right-click → Save Image As is used to drop it into a Word/PDF report.
route.html (topbar link 🧭 Route Optimizer) puts any list of stops in the fastest driving order. It's a browser port of the local Streamlit app in D:\Dropbox\ClaudeCode\route-optimizer, separate from the sidebar's Mapbox Route Planner. Code: web/src/route/; tests: web/test/routeOptimizer.test.js.
lat, lon lines, or a CSV. A blank Start uses the first stop. Finish: one-way (default), round trip, or a fixed location.api/route-google.js, an Edge Function that forwards exactly three operations and holds no key of its own. In dev, vite.config.js serves the same handler.215 & 223 Cree Rd) are geocoded by their first number. A match on a different street type (asked for Crescent, got Road) counts as not found. A match on the street but not the house number is flagged approximate (*).web/src/route/optimize.js. Exact (Held-Karp) up to 12 stops; above that 2-opt + Or-opt with double-bridge restarts, for up to 3 s. Handles asymmetric drive times.@media print, since the CSP allows no popup documents.nominatim.openstreetmap.org and router.project-osrm.org are in connect-src (guarded by test/csp.test.js).Pure static. Vercel serves the Vite-built bundle plus the generated legal-search JSON. The browser makes its own ArcGIS REST queries directly to services.arcgis.com (CORS open). The contaminated-sites CSV at manitoba.ca is proxied through vercel.json rewrites (and a matching Vite dev-server proxy in vite.config.js) because the upstream doesn't send Access-Control-Allow-Origin.
Per-search flow (single direction, one parcel layer):
legal-index.json first. The matching (muni_no, roll_no_txt) keys become a lookup filter for the live Roll Entry query.where clause from the sidebar inputs. Categorical and amendment-status filters resolve through a separate spatial query against the matching overlay layer (using actual polygon geometry, not bbox envelopes — paginated to handle large overlays); the resulting parcel OBJECTID list is ANDed into the parcel query._truncated flag set whenever the cap or exceededTransferLimit triggers.@turf/intersect + @turf/area, mirroring mao-assembly/scripts/pipeline_utils.R::get_multiple_by_area(). Top-2 zonings per parcel (with coverage % per match), top-1 dev-plan designation._soilRiskArea from the containing Risk_Area polygon._soilRating plus quarter-section or river-lot metadata for the Soil table cell.Caching. Data classes use distinct strategies:
npm run legal:index whenever ../mao-scrape/results/parcels.parquet is refreshed.r/build_masc_shards.R and r/build_parcel_masc.R whenever MASC ratings, the river-lot scrape/KMZ, or Roll Entry snapshots are refreshed.mbpsCache. namespace. The municipality / zone-category / dev-plan-category lists are served stale-while-revalidate: a cached list younger than 7 days is used as-is; one older than 7 days (up to 180) is still shown instantly while a single background refresh rewrites it; only a never-cached or >180-day list waits on the live query. The municipality picker also paints from the first list to arrive (cached live list or snapshot-manifest names) rather than waiting on the zoning and record-count probes, and a live muni answer under 150 names (a partial provincial republish) is shown but never cached. Most other lists and live overlays use a 7-day TTL; stable generated/reference overlays such as MASC, MASC Risk Areas, municipal boundaries, river lots, and the per-muni section grid use a 30-day TTL. Quota recovery evicts older namespaced entries before failing. Clear button wipes the namespace.Dependencies (web/package.json):
maplibre-gl — map (Protomaps/OSM Streets, Esri Satellite, NRCan Transportation, and NRCan Elevation)pmtiles — reads the optional MLI historical aerial archive (inert until VITE_MLI_ORTHO_PMTILES_URL is set)@turf/area, @turf/bbox, @turf/intersect, @turf/boolean-point-in-polygon, @turf/length — spatial primitives for the area-weighted join + route distances@mapbox/mapbox-gl-draw — measurement / draw tool on the maptailwindcss + @tailwindcss/vite — utility-first styles (dev)No application database. Two Vercel Edge Functions under api/ stream the legal-index and assessment-index from GitHub Releases (CORS proxy — the indexes are ~130 MB / ~17 MB, past Vercel's static rewrite ceiling). A Mapbox API token drives the optional Route Planner; without it the planner cleanly disables. Generated bulk data (RollEntry fallback shards, parcel-MASC, MASC, landcover shards/tiles, river-lots) lives in the sister mb-parcel-data repo and serves via raw.githubusercontent pinned to an immutable commit (moved off jsDelivr 2026-08-17 — its 50 MB package limit; see MAINTENANCE.md §1b), not from this repo. The monthly data refresh runs monthly-refresh.bat (wrapped by monthly-refresh-wrapper.ps1 for email alerts on failure) and is the only scheduled job; see MAINTENANCE.md for the runbook.
Prerequisites: Node.js 18+ and npm.
cd web
npm install
npm run legal:index # refresh after the MAO scrape output changes
npm run dev
Optional — Mapbox token for the Route Planner. The planner uses Mapbox's Matrix/Directions/Static APIs (free tier ~100k requests/month). Without a token the feature stays disabled and everything else works. To enable it:
cp web/.env.example web/.env.local
# Edit web/.env.local — set VITE_MAPBOX_TOKEN to your pk.* token.
web/.env.local is gitignored. For production, set VITE_MAPBOX_TOKEN in Vercel → Project Settings → Environment Variables. Always URL-restrict the token in the Mapbox dashboard so it can't be used from foreign origins (see MAINTENANCE.md → Mapbox token).
When MASC inputs change, rebuild the static shards from the repo root before starting or deploying:
$env:MASC_SQUARE_CSV = 'D:\path\to\masc_soil_ratings_square_with_latlon_v2.csv'
$env:MASC_RIVERLOT_CSV = 'D:\path\to\masc_soil_ratings_riverlots_v2.csv'
Rscript r/test_masc_utils.R
Rscript r/build_masc_shards.R
Rscript r/build_parcel_masc.R
The square builder keeps special text ranges such as 29A, collapses
exact duplicates, and retains multiple official ratings as a slash-separated
label such as C/H. The conservative/worst code supplies the map colour;
the complete label appears in the map and parcel table.
Open http://localhost:5173. The contaminated-sites CSV proxies through Vite's dev server so Show Enviro works in dev too.
If the project is running from a synced folder and Vite reports Outdated Optimize Dep or an EBUSY rename under node_modules/.vite, start dev with a temp cache:
$env:VITE_CACHE_DIR = Join-Path $env:TEMP 'mbopendata-mb-parcelsearch-vite'
npm run dev -- --host localhost --strictPort --force
Use --strictPort, and kill old servers properly. Without it, a Vite
whose port is taken silently moves to the next free one — it prints
Port 5173 is in use, trying another one... and carries on. Restarting
repeatedly without killing the previous run leaves several servers alive on
5173, 5174, 5175… and the browser then talks to whichever one it was pointed
at, not the one just started. Note pkill -f "vite --port 5173" does NOT
match the real process (it is npx vite …); check with
netstat -ano | grep LISTENING | grep :517 and kill by PID.
A stale server serves a DELETED module as HTTP 200 + HTML. Vite's dev
SPA fallback returns index.html for any unmatched path, so a module graph
still importing a file that a branch switch deleted gets:
GET /src/lib/somethingDeleted.js → 200 OK, Content-Type: text/html
The browser tries to parse HTML as an ES module, that throws, and the module importing it never executes. The symptom is a fully rendered page with no map, no failed network request and nothing obvious in the console — because the request "succeeded". If the app loads but the map never appears, suspect a stale server before suspecting the code, and hard-reload after any branch switch.
Use localhost, not 127.0.0.1. They are different origins to CORS, and
only localhost:5173 is in the R2 bucket's allowlist — see MAINTENANCE.md 1e.
On 127.0.0.1 the Assessment Parcels archive fetch fails with "Failed to
fetch".
To build for production:
npm run build
Output goes to web/dist/ (gitignored). Vercel runs the same command on every push to main.
Prerequisites: R 4.5+ and the packages sf, httr2, shiny, leaflet, DT, dplyr.
source("r/download_parcels.R") # snapshots ROLL_ENTRY + zoning + dev plan
shiny::runApp("r/parcel_search_app.R") # interactive search on the local snapshot
The Shiny app reads the most recent RollEntry_YYYYMMDD.gpkg in the project directory. Shared paths resolve through r/config.R — the repo root is detected from the running script's location, and the external roots (snapshot archive, sister projects) can be overridden on another machine via the environment variables documented there (MB_PARCELSEARCH_ROOT, MAO_SNAPSHOTS_ROOT, MAO_ASSEMBLY_ROOT, MASC_SCRAPE_ROOT, MB_PARCEL_HISTORY_ROOT) instead of editing scripts.
.gpkg, .tif, and .parquet snapshots are gitignored — too large for GitHub and trivially regenerable.Frontage_or_Area from ROLL_ENTRY states an area on ~63% of parcels (160.00 ACRES) and a frontage on the other ~37% (110.00 FEET) — a width, which carries no area information and cannot be converted. The Roll Frontage/Area column reproduces whatever the roll says, verbatim, and the map popup shows it as Roll States. Acres/SF repeat that figure when it is an area and are computed from the polygon otherwise, with Acres Src recording which. On a frontage parcel the acreage is an estimate the roll does not support — and the two can look deceptively alike (roll 26325 in the RM of Ste Anne states 80.07 FEET on a parcel whose polygon measures 79.9 acres). A nominal placeholder area on a large polygon (e.g. 0.01 ACRES on crown/reserve land) is detected and overridden with the geometry figure.logs/verify_areas_full.txt). Past that threshold the app flags the row (⚠ on Acres, plus an Area Check column in the CSV) — a subdivision or consolidation that has reached one half of the record but not the other looks exactly like this.dataLastEditDate, while MAO's map already showed the ±2.3 ac child parcel. The footer surfaces the publish date with this caveat; recently-changed parcels must be confirmed on MAO.Property_Address. Rural parcels often store a quarter-section description here in lieu of a street address.See REPLICATION_GUIDE.md. Originally written for the Winnipeg Socrata source; §14 covers this ArcGIS REST variant including every operational detail an adaptation would need: SQL92 dialect quirks (no TRIM(), <Null> sentinel), bulk-fetch vs per-parcel envelope decision, true polygon-geometry filter resolution, paginated OBJECTID collection, localStorage TTL caching, the Web Worker / rbush optimization deferred to a follow-up perf pass, and the comprehensive muni/PD lookup pattern.
../mao-scrape/results/parcels.parquetLegal description text, Lot, Block, Plan, certificates of title, and (muni_no, roll_no_txt) lookup keys |
| MASC soil ratings | masc_soil_ratings_with_latlon.csv + river-lot scrape/KMZ → web/public/data/masc/, web/public/data/masc-riverlots.json, and web/public/data/parcel-masc/ | Quarter-section and river-lot MASC layer, A-J rating colours, visible rating-letter labels, parcel-level Soil table field, and split-boundary river lots such as De Salaberry / St-Pierre-Jolys |
| MASC Risk Areas / Risk Regions | …/MASC_Risk_Areas/FeatureServer/0 | Official crop-insurance risk-area polygons, Risk Areas overlay labels, and parcel-level Risk Area table field |
| Manitoba Water Rights Licensing (WALLAS) | web43.gov.mb.ca/arcgis/rest/services/WALLAS/wallas_op_external/MapServer layers 7 (tile-drainage areas), 6 / 5 (tile lines + outlets), 2 / 3 (irrigation points of diversion / use) | Licensed tile-drainage footprints for the Tile Drainage overlay, column, and search filter; tile pipe runs and outlets; irrigation licences. CORS-enabled (origin-reflected), so it is queried directly with no proxy |
| Flood, regulatory and waterway layers | Cached by the sister MBFloodMapping project from Data MB, the Manitoba Land Initiative and OpenStreetMap → web/public/data/flood/ (overlay) and mb-parcel-data/flood/ (per-parcel shards) | Five Flood map overlays and the Flood table column: Designated Flood Areas, the RRV Special Management Area, the 1-in-200 year extent, the 1997 / 2009 / 2011 observed extents, and the Winnipeg waterway corridors |
RL-65-FX-5066; description-side lot ranges (RL45/51-BP-626) expandNE-01-13-28-W (≈87 % of the archive). Quarter offers NE/NW/SE/SW plus RL for township river lots; a bare range number matches both meridians, 28W only the west oneZoning Category dropdown (per-muni narrowed)
Status dropdown — Any / Zoning Changed / Dev Plan Changed / Both Changed
DU mode + Min # input — Any DU / 0 DU only (vacant) / Min DU N (≥ N units)
Search · Clear · Export CSV — saved as MAOADF-YYYY-MM-DD-HHMM.csv (local time), the name the R land template looks for
Result count badge
Parcel click popups carry Copy Roll · GPS Coordinates (as in the Winnipeg app); clicking the roll number itself still opens the MAO report
Sales Analysis → Charts opens the Sales Charts page — the land template's CMS charts in LandShiny's layout (map tabs over chart tabs on Land Price/Unit, Agricultural, Total/Per Lot and Water pages), numbered comparables and Land Sets (the grid's ★ is the comp number), exclusion reasons, and a Work file zip of PNGs, CSVs and a self-contained summary.html — see DOCUMENTATION.md §10.0.2
AsmtYr column referenced in the offline pipeline does not exist on the live FeatureServer. The assessment year is parsed from the Asmt_Roll text field (e.g. "2024 Final" → 2024).<Null>; the client treats null, empty, single-space, and <Null> as equivalent.APPLICATION_DATE is 2024-08-29 while its own application tracker (layer 8) runs to the present, so recently-licensed tile may have no polygon yet. Unlicensed and older installations never appear at all. A blank Tile Drainage column is therefore not evidence that land is undrained — the CSV export carries this caveat on its face whenever water-rights data is included.TILE_* detail fields (area, depth, lateral spacing, outlet type) are populated on well under 10% of records — 129 of 1,633 tile polygons carry TILE_AREA. The overlay popup and column tooltip show whichever fields exist rather than a fixed layout.LEGACY_LABEL distinguishes "Area of Proposed Tile Drainage Network" from "Area of Tile Drainage Network", but it is frequently null, so treat every footprint as approximate.esriSpatialRelIntersects counts edge contact as an intersection, and neighbouring survey polygons share edges by construction, so the server-side filters return parcels a licensed area merely grazes. A match covering under 1% of a parcel (MIN_WATER_COVERAGE in main.js) is discarded rather than reported — a clipped rim says nothing true about whether land is drained or watered. The filters then drop those parcels outright once the exact clip has run, and the count line says how many went (e.g. 26 parcels found · 10 dropped (edge overlap only)), so a filtered result never contains a row the column can't vouch for.WALLAS_FILTER_BATCH_SIZE in arcgis.js). Ring winding is normalized first — Esri reads clockwise rings as outer and counter-clockwise as holes, so a naive merge would punch footprints out of their neighbours. Verified against the live service and unit-tested in wallasFilterGeometry.test.js.WATER_SOURCE_NAME and ACQUIFER_NAME are frequently null even on current licences, so the Irrigation Source CSV column is often empty — Irrigation Supply (Groundwater Use / Surface Water Use) is the reliable field.LOCAL_GOVERNMENT column (layer 8 does), so municipal narrowing is spatial. The tile-only filter fetches the muni's extent via one returnExtentOnly request and clips the polygon set before running per-polygon parcel queries; without a municipality selected it has to test every licensed footprint in the province, which is slow but correct.MUNI_WEBSITES and PD_WEBSITES (in main.js) are hand-curated from the province's official Municipal and Planning District contact directories, and nothing refreshes them. A muni with no entry renders as "Muni N/A" — adding an entry promotes it to a working button. A missing entry is better than a wrong one: an absent key degrades honestly, while a rotted URL lights the button and opens a browser error or a domain squatter, which reads as this app's bug. A full check on 2026-08-05 found 15 of 153 URLs dead (6.6% of munis, 26% of planning districts) — repointed where a replacement existed, removed otherwise. Half the failures returned HTTP 200, and many municipalities legitimately brand off-name, so an automated checker would flag more good entries than bad; re-derive from the provincial directory after the October 2026 municipal elections instead.