Land Parcel Registry · AfifAI
Case study · PropTech & GIS · 2025

A land-parcel registry that fills in its own addresses

A back office for an Israeli land and real-estate company to manage its parcel portfolio by block and parcel number. A background worker looks up each parcel's address from the national GIS service, and one click opens the parcel highlighted on the national map. Before, both were done by hand.

.NET 8 Web APIEF Core · SQL ServerReact 18 · TypeScriptAnt Design · Redux ToolkitNational GIS APIsAWS S3JWT
Client
Israeli land & real-estate company (confidential)
My role
Team lead & architect (hands-on)
Timeline
Apr – Aug 2025 · ≈4 months, 2 repos
Status
Delivered
At a glance
≈25 h → ≈10 minaddress lookup for 1,000 parcels: manual GIS searches vs. an unattended background job (estimate)
≈1 min → 1 clickto see a parcel on the national map, with its block and parcel highlighted (estimate)
12 + 8filters and sortable columns, all kept in the URL so a view can be shared
0duplicate parcels possible: unique keys on block + parcel and on sub-parcels
23REST endpoints across 5 controllers
11tables, with 55 land-use codes and 5 ownership types seeded
122national map layers configured in the embedded map
3 weeksfrom an empty API to working CRUD, media and login
The problem

A parcel portfolio managed by hand

In Israel, land is identified by a block number and a parcel number. The company tracks a large portfolio of them, each with an ownership type, a registration method, a land-use designation, an area, sub-parcels and supporting documents. Two everyday tasks were manual. Finding a parcel's street address meant searching the national GIS portal one parcel at a time and copying the result. Seeing where a parcel is meant opening the portal again and typing the numbers in. Nobody could easily tell which records were still missing data.

What I owned

Architecture, back end and the admin app

  • Designed the layered .NET 8 solution (API, services, storage, DB) and the SQL Server schema, including the persisted completeness flag and the unique keys.
  • Built the address-enrichment worker that calls the national GIS search API in throttled, resumable batches.
  • Built the React admin: parcel list with 12 filters, 8 sortable columns, URL-synced state, a parcel page with media, and the embedded national map.
  • Built JWT sign-in with refresh tokens, and media upload to S3.
  • Set up the Dev → QA → Stage → Production branch flow and the lint pre-commit hook.
Architecture

One API, one worker, and the national GIS as an upstream

The diagram is wider than the screen: swipe sideways to see all of it →

The worker runs inside the API host, so there is nothing extra to deploy. It keeps the raw GIS response next to the parsed address for auditing, and the list endpoint leaves that column out so list pages stay light.

Design decisions

Let the database and the worker do the boring parts

Completeness as a persisted computed column

A record counts as “complete” when it has an address, an area and a land use. SQL Server computes and stores that flag, so the list can show and sort by status with no application code to keep in sync. Trade-off: changing the rule needs a schema change, not a config change.

A resumable worker, not a one-off import

The worker picks parcels that have never been updated, in small ordered batches, and spaces its calls 100 ms apart. New parcels get enriched automatically, and a restart carries on from where it stopped. Trade-off: addresses fill in over minutes instead of instantly.

Embed the national map instead of drawing a new one

The admin loads the national GIS JavaScript map and highlights the block and then the parcel. The client gets the official cadastral layers for free. Trade-off: a third-party script with a jQuery dependency and its own load timing, which needed careful handling (see below).

All list state lives in the URL

Filters, sorting and paging are written to the query string and read back on load. A filtered view survives a refresh and can be sent to a colleague as a link. Trade-off: more parsing code, including multi-value filters.

Engineering log

Problems I hit, and the fixes

Apr 23

Sub-parcels didn't fit in the parcel row

A parcel can have any number of sub-parcels. Keeping them inside the parcel record made filtering and validation awkward.

Moved sub-parcels into their own table with a unique (parcel, sub-parcel) key. On update the set is replaced as a whole, so there are no partial edits.

Apr 23

Duplicate parcels

The same block and parcel could be entered twice. That would split media and history across two records.

Added a unique index on block + parcel, so the database rejects duplicates no matter which path the data comes in by.

Apr 27 – May 12

Lost filters on refresh

Users lost their filters and page whenever they refreshed or followed a link. Defaults also overwrote values that came from the URL.

Moved table state to the query string with typed parsing. Defaults now apply only when a value is missing. Page size is standardised at 100 and the default sort is block, then parcel.

Apr 29 – Aug 11

The map wasn't ready when the data arrived

The national map script loads asynchronously. Highlighting a parcel before the map had initialised failed without any error.

Load the script on demand, poll for the map API every 100 ms (7 s timeout), and run the block and parcel highlight only once both the map and the parcel data are ready.

Background job

One bad parcel shouldn't stop the run

Some block/parcel pairs return no match or an error from the GIS service.

Each parcel is wrapped on its own and logged with its block and parcel. A failed batch backs off before the next cycle. Empty results never overwrite an existing address.

Apr 28

Media as links and as files

Each parcel needs documents: external links, uploaded files, or both.

Built one media form that sends multipart/form-data to the API, which stores files in S3. On edit, the existing file is kept unless a new one is uploaded.

Before / after

Getting one parcel's address and location

Step times are conservative estimates for illustration, not measurements. The “after” address step runs with nobody involved.

Outcome

Where it landed

Task
Before
After
Address for 1,000 parcels
≈25 h of copy-paste from the GIS portal
≈10 min, unattended background jobestimate
See a parcel on the map
≈1 min: open the portal, type block and parcel
1 click from the list, already highlightedestimate
Which records are incomplete?
Check each row by hand
Status column, computed in the databasederived
Duplicate parcels
Possible
Rejected by unique keysderived
Sharing a filtered view
Describe the filters in a message
Send the URLderived
Parcel documents
Scattered across folders
Links and files attached to each parcelderived
Stack

Technology stack

Back end
.NET 8 Web API, layered services, AutoMapper, NLog, Swagger
Data
SQL Server, EF Core 8, computed columns, SSDT schema compare
Background work
Hosted BackgroundService, throttled HTTP client
Front end
React 18, TypeScript, Vite, Ant Design, Redux Toolkit, react-hook-form, i18next (Hebrew, RTL)
Maps
National GIS JavaScript API and search API
Security & files
JWT with refresh tokens, AWS S3

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects