SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
5.2 KB · 178 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Données agrégées par municipalité pour les pages programmatiques SEO.3// Les agrégats ne changent qu'au rebuild de la DB : cache mémoire par processus.45import { getDb } from "./db";6import { slugify } from "./seo";78export interface MunSummary {9  nom: string;10  slug: string;11  n: number;12  lat: number;13  lng: number;14}1516export interface TypeStats {17  type: string;18  n: number;19  total2026: number;20  mediane2026: number | null;21}2223export interface MunStats {24  nom: string;25  slug: string;26  n: number;27  total2026: number;28  total2021: number;29  mediane2026: number | null;30  croissancePct: number | null;31  parType: TypeStats[];32}3334export interface SampleUnit {35  id_provinc: string;36  adresse: string | null;37  apt: string | null;38  type_prop: string;39  est_2026: number | null;40  annee_construction: number | null;41  aire_etages_m2: number | null;42}4344let munList: MunSummary[] | null = null;45let munBySlugMap: Map<string, MunSummary> | null = null;46const statsCache = new Map<string, MunStats>();4748/** Toutes les municipalités (nom, slug, nb d'unités, centroïde) — triées par taille. */49export function allMunicipalities(): MunSummary[] {50  if (!munList) {51    const rows = getDb()52      .prepare(53        `SELECT municipalite AS nom, COUNT(*) AS n,54                AVG(lat) AS lat, AVG(lng) AS lng55         FROM units WHERE municipalite IS NOT NULL AND municipalite != ''56         GROUP BY municipalite ORDER BY n DESC`57      )58      .all() as { nom: string; n: number; lat: number; lng: number }[];59    munList = rows.map((r) => ({ ...r, slug: slugify(r.nom) }));60    munBySlugMap = new Map(munList.map((m) => [m.slug, m]));61  }62  return munList;63}6465export function munBySlug(slug: string): MunSummary | undefined {66  allMunicipalities();67  return munBySlugMap!.get(slug);68}6970function median2026(nom: string, type?: string): number | null {71  const db = getDb();72  const where = type73    ? "municipalite = ? AND type_prop = ?"74    : "municipalite = ?";75  const args: (string | number)[] = type ? [nom, type] : [nom];76  const cnt = (77    db.prepare(`SELECT COUNT(*) AS c FROM units WHERE ${where} AND est_2026 IS NOT NULL`).get(...args) as { c: number }78  ).c;79  if (!cnt) return null;80  const row = db81    .prepare(82      `SELECT est_2026 AS v FROM units WHERE ${where} AND est_2026 IS NOT NULL83       ORDER BY est_2026 LIMIT 1 OFFSET ?`84    )85    .get(...args, Math.floor(cnt / 2)) as { v: number } | undefined;86  return row?.v ?? null;87}8889/** Statistiques complètes d'une municipalité (cache mémoire). */90export function munStats(nom: string): MunStats | null {91  const cached = statsCache.get(nom);92  if (cached) return cached;93  const db = getDb();94  const base = db95    .prepare(96      `SELECT COUNT(*) AS n, SUM(est_2026) AS t26, SUM(est_2021) AS t2197       FROM units WHERE municipalite = ?`98    )99    .get(nom) as { n: number; t26: number | null; t21: number | null };100  if (!base.n) return null;101  const types = db102    .prepare(103      `SELECT type_prop AS type, COUNT(*) AS n, SUM(est_2026) AS total2026104       FROM units WHERE municipalite = ?105       GROUP BY type_prop ORDER BY n DESC`106    )107    .all(nom) as { type: string; n: number; total2026: number }[];108  const stats: MunStats = {109    nom,110    slug: slugify(nom),111    n: base.n,112    total2026: base.t26 ?? 0,113    total2021: base.t21 ?? 0,114    mediane2026: median2026(nom),115    croissancePct:116      base.t21 && base.t26 ? ((base.t26 - base.t21) / base.t21) * 100 : null,117    parType: types.map((t) => ({118      ...t,119      mediane2026: median2026(nom, t.type),120    })),121  };122  statsCache.set(nom, stats);123  return stats;124}125126/** Échantillon de propriétés adressées (les plus valorisées) pour le maillage interne. */127export function sampleUnits(128  nom: string,129  type: string | null,130  limit = 24131): SampleUnit[] {132  const where = type133    ? "municipalite = ? AND type_prop = ?"134    : "municipalite = ?";135  const args: string[] = type ? [nom, type] : [nom];136  return getDb()137    .prepare(138      `SELECT id_provinc, adresse, apt, type_prop, est_2026,139              annee_construction, aire_etages_m2140       FROM units141       WHERE ${where} AND adresse IS NOT NULL AND est_2026 IS NOT NULL142       ORDER BY est_2026 DESC LIMIT ?`143    )144    .all(...args, limit) as SampleUnit[];145}146147let typePairs: { nom: string; type: string }[] | null = null;148149/** Paires municipalité × type existantes (pour le sitemap des pages types). */150export function allMunTypePairs(): { nom: string; type: string }[] {151  if (!typePairs) {152    typePairs = getDb()153      .prepare(154        `SELECT municipalite AS nom, type_prop AS type155         FROM units WHERE municipalite IS NOT NULL AND municipalite != ''156         GROUP BY municipalite, type_prop`157      )158      .all() as { nom: string; type: string }[];159  }160  return typePairs;161}162163/** Les municipalités les plus proches du centroïde (maillage entre pages voisines). */164export function neighborMunicipalities(nom: string, k = 10): MunSummary[] {165  const all = allMunicipalities();166  const me = all.find((m) => m.nom === nom);167  if (!me) return [];168  return all169    .filter((m) => m.nom !== nom)170    .map((m) => ({171      m,172      d: (m.lat - me.lat) ** 2 + 0.49 * (m.lng - me.lng) ** 2,173    }))174    .sort((a, b) => a.d - b.d)175    .slice(0, k)176    .map((x) => x.m);177}178