SPB Git

spb/valoplex Public

ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.

TypeScript 90.3% Python 7.1% CSS 2.5%
3.9 KB · 145 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2import Database from "better-sqlite3";3import path from "path";45let db: Database.Database | null = null;67export function getDb(): Database.Database {8  if (!db) {9    const p =10      process.env.VALOPLEX_DB ?? path.join(process.cwd(), "data", "valoplex.db");11    db = new Database(p, { fileMustExist: true });12    db.pragma("journal_mode = WAL");13  }14  return db;15}1617export interface UnitRow {18  id_provinc: string;19  adresse: string | null;20  apt: string | null;21  municipalite: string | null;22  arrond: string | null;23  code_mun: string | null;24  lat: number;25  lng: number;26  cubf: number | null;27  cubf_libelle: string | null;28  type_prop: string;29  annee_construction: number | null;30  annee_estimee: string | null;31  aire_etages_m2: number | null;32  superficie_terrain_m2: number | null;33  front_terrain_m: number | null;34  nb_etages: number | null;35  nb_logements: number | null;36  nb_locaux_non_resid: number | null;37  nb_chambres_locatives: number | null;38  lien_physique: string | null;39  genre_construction: string | null;40  matricule: string | null;41  unite_voisinage: string | null;42  n_adresses: number | null;43  dat_cond_marche: string | null;44  valeur_terrain: number | null;45  valeur_batiment: number | null;46  valeur_role: number | null;47  valeur_anterieure: number | null;48  adresses_json: string | null;49  est_2021: number | null;50  est_2022: number | null;51  est_2023: number | null;52  est_2024: number | null;53  est_2025: number | null;54  est_2026: number | null;55  p10: number | null;56  p90: number | null;57  est_hedo: number | null;58}5960export interface TxRow {61  id: string;62  date: string;63  amount: number;64  street: string | null;65  city: string | null;66  lat: number;67  lng: number;68  property_type: string | null;69  year_built: number | null;70  floor_area: number | null;71  building_type: string | null;72  id_provinc: string | null;73  valeur_role: number | null;74  land_area: number | null;75  portes: number | null;76}7778export function getUnit(id: string): UnitRow | undefined {79  return getDb()80    .prepare("SELECT * FROM units WHERE id_provinc = ?")81    .get(id) as UnitRow | undefined;82}8384/** Candidats comparables dans une boîte englobante autour du point. */85export function getCandidates(86  lat: number,87  lng: number,88  halfDeg: number,89  sinceDate: string,90  limit = 50091): TxRow[] {92  return getDb()93    .prepare(94      `SELECT * FROM transactions95       WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?96         AND date >= ? AND amount >= 5000097       LIMIT ?`98    )99    .all(lat - halfDeg, lat + halfDeg, lng - halfDeg, lng + halfDeg, sinceDate, limit) as TxRow[];100}101102export function getMarketIndex(typeProp: string): { month: string; idx: number }[] {103  return getDb()104    .prepare(105      "SELECT month, idx FROM market_index WHERE type_prop = ? ORDER BY month"106    )107    .all(typeProp) as { month: string; idx: number }[];108}109110export function searchUnits(q: string, limit = 8): (UnitRow & { score: number })[] {111  const cleaned = q112    .replace(/[^\p{L}\p{N}\s'-]/gu, " ")113    .trim()114    .split(/\s+/)115    .filter((t) => t.length > 0)116    .map((t) => `"${t}"*`)117    .join(" ");118  if (!cleaned) return [];119  return getDb()120    .prepare(121      `SELECT u.*, bm25(units_fts) AS score122       FROM units_fts JOIN units u ON u.rowid = units_fts.rowid123       WHERE units_fts MATCH ?124       ORDER BY score LIMIT ?`125    )126    .all(cleaned, limit) as (UnitRow & { score: number })[];127}128129export function municipalityCenter(130  name: string131): { lat: number; lng: number; n: number } | undefined {132  return getDb()133    .prepare(134      `SELECT AVG(lat) AS lat, AVG(lng) AS lng, COUNT(*) AS n135       FROM units WHERE municipalite = ? COLLATE NOCASE`136    )137    .get(name) as { lat: number; lng: number; n: number } | undefined;138}139140export function saveLead(email: string, unitId: string | null, estimate: number | null): void {141  getDb()142    .prepare("INSERT INTO leads (email, unit_id, estimate) VALUES (?, ?, ?)")143    .run(email, unitId, estimate);144}145