# Toit-Ka — rent or buy a roof in Québec, one place ![Listings](https://img.shields.io/badge/listings-85%20503-e07a3f?style=flat-square) ![For rent](https://img.shields.io/badge/for%20rent-22%20881-d9f26b?style=flat-square&labelColor=141814) ![For sale](https://img.shields.io/badge/for%20sale-62%20622-e23744?style=flat-square&labelColor=141814) ![Sources](https://img.shields.io/badge/sources-229%2B-181410?style=flat-square) ![Avg rent](https://img.shields.io/badge/avg%20rent-%241%20732%2Fmo-1c5c41?style=flat-square) ![Avg price](https://img.shields.io/badge/avg%20price-%24683%20945-b3202b?style=flat-square) ![Backend](https://img.shields.io/badge/backend-FastAPI%20%2B%20SQLite-009688?style=flat-square) ![Frontend](https://img.shields.io/badge/frontend-React%2018%20%2B%20Vite%20%2B%20TS-61dafb?style=flat-square&labelColor=20232a) ![Maps](https://img.shields.io/badge/maps-Ka%20Maps%20(Mapbox%20GL%203D)-4264fb?style=flat-square) ![Lang](https://img.shields.io/badge/lang-fran%C3%A7ais%20(QC)-blue?style=flat-square) **Toit-Ka** ([www.toit-ka.com](https://www.toit-ka.com)) is the merger of two Québec real-estate aggregators into a single platform where users search **for rent OR for sale** in one place: - **[Lou-Ka](https://www.lou-ka.com)** — ~23,000 rental listings aggregated from 200+ property managers, brokers and platforms; - **[Immo-Ka](https://www.immo-ka.com)** — ~63,000 for-sale properties aggregated from 29 networks (RE/MAX, Via Capitale, Sutton, Proprio Direct, Centris feeds…). Toit-Ka **does not scrape anything itself**: it consumes read-only replicas of both production databases and unifies them through a normalization ETL. The two original sites keep running untouched. Every listing card links back to the original ad at the source. ## Architecture ``` M3U96b (prod) M4M64a (prod) lou-ka louka.db immo-ka immoka.db │ sqlite3 .backup + rsync (LAN) │ ← read-only, every 30 min ▼ ▼ M3U96a ~/apps/toit-ka data/replicas/{louka,immoka}.db │ ETL (toitka/etl.py) │ · inherited visibility rules (Centris dedup, plausible prices) │ · canonical city mapping (toitka/villes.py — 3,300+ raw variants) │ · unified type vocabulary + HTML-entity repair ▼ data/toitka.db — one `listings` table, `transaction_type ∈ {louer, acheter}` │ FastAPI (toitka/web.py) ── JSON API + server-side SEO rendering (seo.py) │ React 18 SPA (frontend/) ── Louer/Acheter toggle drives the site accent │ ngrok ── https://www.toit-ka.com ``` ### The city-normalization engine (`toitka/villes.py`) The two corpora carried **~3,300 distinct raw city strings** for roughly 1,000 real municipalities. Deterministic rules + a slug-grouping election pass fix, among others: orphan parentheses (`Brossard )`), never-closed parentheses (`Gatineau (Hôpital`), duplicated names (`Gatineau (Gatineau)`), boroughs glued in both orders (`Montréal (Ville-Marie)` vs `Ville-Marie (…)`), merged municipalities (`Saint-Hubert (Longueuil)`), English variants (`Québec City`, `Montreal Downtown`, `City Of Montréal`), lost accents/hyphens (`Trois Rivieres`, `Mont Tremblant`), mojibake (`Montrã©al`) and marketing labels (`Longueuil / South Shore`). The full raw→canonical mapping is persisted in the `city_map` table for auditing (`python3 run.py villes`). ### Dual-accent design system Single "sharp editorial" design system (Space Grotesk / Inter / JetBrains Mono, ink borders, offset shadows) with an accent that **follows the universe**: Lou-Ka electric lime in *Louer* mode, Immo-Ka red in *Acheter* mode, terracotta for the Toit-Ka brand itself. The switch is one `html[data-mode]` attribute. ## Features - Home page with a central **Louer / Acheter toggle** — filters, price steps, quick chips and stats all adapt to the selected universe. - Search by city (canonical), price, type (3½…6½+, Studio, Condo, Maison, Plex, Terrain…), bedrooms/bathrooms, area, pets, furnished, free text. - List + **3D map view** (Ka Maps / Mapbox GL) with per-universe markers (lime rent pills, ink sale pills), "search this area", React previews. - Full listing page: gallery + lightbox, specs, description, walkability rings mini-map, link to the original ad. - **Programmatic SEO** rendered server-side: `/louer/{ville}`, `/acheter/{ville}`, `/{tx}/{ville}/{type}`, `/{tx}/type/{type}` with average and median rent/price, breadcrumbs, JSON-LD, sitemaps, 301 canonical slugs, 410 for withdrawn listings. - **KA ID single sign-on** (groupe-ka.com identity hub) + favorites stored centrally in "Mon univers Ka". - Mobile-first, French (Québec). ## Repository layout ``` run.py CLI: serve | etl | watch | replicate | villes toitka/ db.py unified schema (toitka.db) + read-only source connections villes.py canonical city mapping engine typologie.py unified type vocabulary (rent + sale) etl.py replicas -> toitka.db (visibility rules, lifecycle) replicate.py LAN pull of production snapshots (sqlite .backup + rsync) web.py FastAPI JSON API + SPA serving seo.py server-side SEO rendering, robots.txt, sitemaps auth.py KA ID SSO (JWT HS256, stdlib only) hubprofile.py member profile read from the Groupe KA hub hubfav.py favorites.py central favorites ("Mon univers Ka") frontend/ React 18 + Vite + TypeScript SPA (dual-accent theme) ``` ## Running locally ```bash # point the ETL at local copies of the source DBs (.env) python3 run.py etl # build data/toitka.db python3 run.py serve 8097 # http://localhost:8097 cd frontend && npm install && npm run build # SPA served by the backend ``` ## Production (MacLustr cluster) - Node **M3U96a**, `~/apps/toit-ka`, PM2: `toit-ka-web` (port 8097), `toit-ka-etl` (replicate + ETL every 30 min), `toit-ka-ngrok` (`ngrok http --url=www.toit-ka.com 8097`). - Health: `GET /api/health`. ## Author **Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai) Toit-Ka is a **Groupe-Ka** application ([groupe-ka.com](https://www.groupe-ka.com)). Listing data belongs to its respective sources; every card links to the original ad.