spb/vrai-prix Public
Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 96.7%
CSS 3.2%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Rapport d'évaluation détaillé — format professionnel (présentation bancaire).4 * Structure calquée sur un rapport d'évaluation : page titre, sommaire exécutif,5 * mandat et portée, définition de la valeur marchande, méthodologie, description6 * de la propriété, grille d'ajustement des comparables, analyse hédonique,7 * réconciliation, hypothèses et conditions limitatives, certification.8 * Ton sobre, encadrés honnêtes : AVM statistique — n'est PAS un rapport OEAQ.9 */10import PDFDocument from "pdfkit";11import path from "path";12import type { UnitEstimate } from "./estimator";1314const INK = "#141814";15const INK2 = "#4d5551";16const INK3 = "#8b928c";17const SURFACE2 = "#faf9f5";18const GREEN = "#9e2a25";19const GREEN_DEEP = "#771f1b";20const LIME = "#ff5148";2122const W = 612;23const H = 792;24const M = 56;25const CW = W - 2 * M;2627const F = (f: string) => path.join(process.cwd(), "assets", "fonts", f);28const fmt = (v: number | null | undefined) =>29 v == null30 ? "—"31 : new Intl.NumberFormat("fr-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0 }).format(v);32const signed = (v: number) => `${v >= 0 ? "+" : "−"}${fmt(Math.abs(Math.round(v)))}`;33const CONF_TXT: Record<string, string> = {34 A: "Très fiable", B: "Fiable", C: "Indicative", D: "Peu fiable — marché mince",35};3637type Doc = InstanceType<typeof PDFDocument>;3839interface Ctx {40 doc: Doc;41 page: number;42 dossier: string;43 generated: string;44}4546function header(c: Ctx) {47 const { doc } = c;48 doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Vrai", M, 26, { lineBreak: false });49 const w = doc.widthOfString("Vrai");50 doc.rect(M + w + 2, 25, doc.widthOfString("Prix") + 6, 13).fill(INK);51 doc.fillColor(LIME).text("Prix", M + w + 5, 26, { lineBreak: false });52 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)53 .text("RAPPORT D'ÉVALUATION DÉTAILLÉ — FORMAT PROFESSIONNEL", M + 70, 29, { characterSpacing: 0.8, lineBreak: false });54 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)55 .text(`DOSSIER ${c.dossier}`, W - M - 160, 29, { width: 160, align: "right" });56 doc.rect(M, 44, CW, 0.8).fill(INK);57}5859function footer(c: Ctx, total: number) {60 const { doc } = c;61 doc.rect(M, H - 52, CW, 0.8).fill(INK);62 doc.font("JB-Reg").fontSize(6).fillColor(INK3).text(63 "ESTIMATION STATISTIQUE AUTOMATISÉE (AVM) — NE CONSTITUE PAS UN RAPPORT D'ÉVALUATION AU SENS DES NORMES DE L'OEAQ · VRAI-PRIX.COM · CONTACT@SPBOUCHER.AI",64 M, H - 44, { width: CW - 50, characterSpacing: 0.3, lineBreak: false });65 doc.font("JB-Bold").fontSize(7).fillColor(INK)66 .text(`${c.page} / ${total}`, W - M - 40, H - 45, { width: 40, align: "right" });67}6869function newPage(c: Ctx, total: number) {70 c.doc.addPage({ size: "LETTER", margin: 0 });71 c.page += 1;72 c.doc.rect(0, 0, W, H).fill("#ffffff");73 header(c);74 footer(c, total);75 return 62; // y de départ76}7778function sectionTitle(c: Ctx, num: string, title: string, y: number): number {79 const { doc } = c;80 doc.font("JB-Bold").fontSize(8).fillColor(GREEN)81 .text(num, M, y + 2, { lineBreak: false });82 doc.font("SG-Bold").fontSize(13).fillColor(INK)83 .text(title.toUpperCase(), M + 30, y, { lineBreak: false });84 doc.rect(M, y + 19, CW, 1.4).fill(INK);85 doc.rect(M, y + 21.5, 60, 2.2).fill(LIME);86 return y + 32;87}8889function para(c: Ctx, txt: string, y: number, opts?: { size?: number; color?: string; width?: number }): number {90 const { doc } = c;91 doc.font("Inter").fontSize(opts?.size ?? 8.6).fillColor(opts?.color ?? INK2)92 .text(txt, M, y, { width: opts?.width ?? CW, lineGap: 2.2, align: "justify" });93 return doc.y + 8;94}9596function kvTable(c: Ctx, rows: [string, string][], y: number, colW = CW): number {97 const { doc } = c;98 const rh = 17;99 rows.forEach(([k, v], i) => {100 const ry = y + i * rh;101 if (i % 2 === 0) doc.rect(M, ry, colW, rh).fill(SURFACE2);102 doc.font("JB-Bold").fontSize(6.6).fillColor(INK3)103 .text(k.toUpperCase(), M + 8, ry + 5, { characterSpacing: 0.5, lineBreak: false });104 doc.font("SG-Bold").fontSize(8.6).fillColor(INK)105 .text(v, M + colW * 0.42, ry + 4, { width: colW * 0.58 - 10, height: 11, ellipsis: true });106 });107 doc.rect(M, y, colW, rows.length * rh).lineWidth(0.9).stroke(INK);108 return y + rows.length * rh + 10;109}110111export async function buildProReport(112 data: UnitEstimate,113 marketIndex: { month: string; idx: number }[]114): Promise<Buffer> {115 const u = data.unit!;116 const s = u.specs;117 const r = data.result;118 const generated = new Date().toISOString().slice(0, 10);119 const dossier = `VP-${u.id.slice(0, 8)}-${generated.replaceAll("-", "")}`;120 const TOTAL = 6;121122 const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: `Rapport d'évaluation détaillé — ${u.adresse ?? ""}` } });123 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));124 doc.registerFont("SG-Med", F("SpaceGrotesk-Medium.ttf"));125 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));126 doc.registerFont("JB-Bold", F("JetBrainsMono-Bold.ttf"));127 doc.registerFont("Inter", F("Inter-Regular.ttf"));128129 const chunks: Buffer[] = [];130 doc.on("data", (ch: Buffer) => chunks.push(ch));131 const done = new Promise<Buffer>((res) => doc.on("end", () => res(Buffer.concat(chunks))));132 const c: Ctx = { doc, page: 1, dossier, generated };133134 /* ------------------------------- PAGE TITRE ------------------------------- */135 doc.rect(0, 0, W, H).fill("#ffffff");136 doc.rect(0, 0, W, 6).fill(INK);137 doc.rect(0, 6, W, 2).fill(LIME);138 // logo sobre139 doc.font("SG-Bold").fontSize(26).fillColor(INK).text("Vrai", M, 70, { lineBreak: false });140 const lw = doc.widthOfString("Vrai");141 doc.rect(M + lw + 4, 68, doc.widthOfString("Prix") + 14, 32).fill(INK);142 doc.fillColor(LIME).text("Prix", M + lw + 11, 70, { lineBreak: false });143 doc.font("JB-Reg").fontSize(7.5).fillColor(INK3)144 .text("MOTEUR D'ÉVALUATION IMMOBILIÈRE STATISTIQUE · QUÉBEC", M, 108, { characterSpacing: 1.2 });145146 doc.rect(M, 168, CW, 1.4).fill(INK);147 doc.font("SG-Bold").fontSize(30).fillColor(INK)148 .text("RAPPORT D'ÉVALUATION\nDÉTAILLÉ", M, 188, { lineGap: 4 });149 doc.font("Inter").fontSize(10.5).fillColor(INK2)150 .text("Approche par comparaison directe et modélisation hédonique de masse", M, 262);151 doc.rect(M, 288, CW, 1.4).fill(INK);152153 // bloc propriété154 doc.font("JB-Bold").fontSize(7.5).fillColor(INK3).text("PROPRIÉTÉ ÉVALUÉE", M, 318, { characterSpacing: 1.2 });155 doc.font("SG-Bold").fontSize(17).fillColor(INK)156 .text(`${u.adresse ?? ""}${s.apt ? ` app. ${s.apt}` : ""}`, M, 332);157 doc.font("SG-Med").fontSize(12).fillColor(INK2)158 .text(`${u.municipalite ?? ""} (Québec) · Matricule ${s.matricule ?? "—"}`, M, doc.y + 2);159160 // encadré conclusion161 const by = 420;162 doc.roundedRect(M, by, CW, 128, 6).lineWidth(1.6).fillAndStroke(SURFACE2, INK);163 doc.font("JB-Bold").fontSize(7.5).fillColor(GREEN_DEEP)164 .text("CONCLUSION DE LA VALEUR MARCHANDE ESTIMÉE", M + 20, by + 16, { characterSpacing: 1.2 });165 doc.font("SG-Bold").fontSize(38).fillColor(INK).text(fmt(r.estimate), M + 20, by + 32);166 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("FOURCHETTE (P10-P90)", M + 20, by + 84, { lineBreak: false });167 doc.font("SG-Bold").fontSize(11).fillColor(INK).text(`${fmt(r.low)} → ${fmt(r.high)}`, M + 20, by + 96, { lineBreak: false });168 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("DATE D'EFFET", M + 260, by + 84, { lineBreak: false });169 doc.font("SG-Bold").fontSize(11).fillColor(INK).text(generated, M + 260, by + 96, { lineBreak: false });170 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("FIABILITÉ", M + 380, by + 84, { lineBreak: false });171 doc.font("SG-Bold").fontSize(11).fillColor(INK)172 .text(`${r.confidenceLevel} · ${r.confidencePct} %`, M + 380, by + 96, { lineBreak: false });173174 // préparé pour / par175 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("PRÉPARÉ POUR", M, 585, { characterSpacing: 1 });176 doc.rect(M, 610, 220, 0.9).fill(INK2);177 doc.font("Inter").fontSize(7).fillColor(INK3).text("(destinataire / institution financière)", M, 615);178 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("PRÉPARÉ PAR", M + 300, 585, { characterSpacing: 1 });179 doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Vrai-Prix — moteur d'évaluation statistique", M + 300, 597);180 doc.font("Inter").fontSize(8).fillColor(INK2).text("Simon-Pierre Boucher · contact@spboucher.ai", M + 300, 611);181 doc.font("Inter").fontSize(8).fillColor(INK2).text(`Dossier ${dossier} · Généré le ${generated}`, M + 300, 623);182183 // avis184 doc.roundedRect(M, 668, CW, 54, 5).lineWidth(1).stroke(INK);185 doc.font("JB-Bold").fontSize(6.5).fillColor(INK).text("AVIS IMPORTANT", M + 12, 678, { characterSpacing: 1 });186 doc.font("Inter").fontSize(7.4).fillColor(INK2).text(187 "Ce rapport est produit automatiquement par un modèle d'évaluation de masse (AVM). Il est structuré selon les usages de la " +188 "pratique de l'évaluation afin d'en faciliter la lecture, mais il ne constitue pas un rapport d'évaluation préparé par un " +189 "évaluateur agréé (OEAQ) et aucune inspection de la propriété n'a été effectuée. Voir les hypothèses et conditions limitatives, section 7.",190 M + 12, 689, { width: CW - 24, lineGap: 1.5 });191 footer(c, TOTAL);192193 /* ------------------------- PAGE 2 : SOMMAIRE EXÉCUTIF ------------------------- */194 let y = newPage(c, TOTAL);195 y = sectionTitle(c, "1.", "Sommaire exécutif", y);196 y = kvTable(c, [197 ["Adresse", `${u.adresse ?? ""}${s.apt ? ` app. ${s.apt}` : ""}, ${u.municipalite ?? ""}`],198 ["Identifiant provincial", u.id],199 ["Matricule", s.matricule ?? "—"],200 ["Usage prédominant (CUBF)", `${s.cubf ?? "—"} — ${s.cubfLibelle ?? "—"}`],201 ["Droit évalué (présumé)", "Pleine propriété, libre de toute charge"],202 ["Date d'effet de l'estimation", generated],203 ["Méthodes utilisées", "Comparaison directe (ajustée) et modèle hédonique de masse"],204 ["Valeur estimée", fmt(r.estimate)],205 ["Fourchette P10-P90", `${fmt(r.low)} → ${fmt(r.high)}`],206 ["Indice de fiabilité", `${r.confidenceLevel} (${r.confidencePct} %) — ${CONF_TXT[r.confidenceLevel]}`],207 ["Évaluation municipale (rôle 2026)", fmt(u.valeurRole)],208 ["Écart estimation / rôle", u.valeurRole ? `${((r.estimate / u.valeurRole - 1) * 100).toFixed(1)} %` : "—"],209 ], y);210 y = para(c,211 "La valeur conclue résulte de la réconciliation pondérée de deux approches indépendantes : un modèle hédonique de " +212 `masse entraîné sur environ 690 000 transactions québécoises (pondération ${(r.modelWeight * 100).toFixed(0)} %) et l'analyse de ` +213 `${r.nCompsUsed} ventes comparables récentes situées à proximité du sujet, individuellement ajustées pour les conditions de ` +214 `marché, la superficie habitable et l'âge (pondération ${((1 - r.modelWeight) * 100).toFixed(0)} %). La dispersion des comparables ` +215 `ajustés est de ±${r.compsDispersionPct ?? "—"} %. L'indice de fiabilité intègre le nombre de comparables, leur cohérence et la ` +216 "largeur de la fourchette d'incertitude du modèle.", y);217218 y = sectionTitle(c, "2.", "Mandat, portée et définition de la valeur", y + 4);219 y = para(c,220 "Mandat — Estimer la valeur marchande la plus probable de la propriété identifiée ci-dessus, à la date d'effet, aux fins " +221 "d'information et d'appui à une démarche de financement. Portée — Analyse automatisée des données publiques du rôle " +222 "d'évaluation foncière (MAMH) et des transactions publiées ; aucune inspection, aucune vérification de titres.", y);223 y = para(c,224 "Définition de la valeur marchande — Le prix le plus probable, à la date d'effet, moyennant paiement comptant ou " +225 "conditions équivalentes, pour lequel la propriété devrait se vendre sur un marché concurrentiel et ouvert, dans le cadre " +226 "d'une transaction sans lien de dépendance entre un acheteur et un vendeur prudents et informés, aucune des parties " +227 "n'étant contrainte d'agir et un délai de mise en marché raisonnable étant accordé.", y);228 y = para(c,229 "Approches écartées — L'approche du coût n'est pas appliquée (absence de données fiables sur le coût de remplacement " +230 "déprécié sans inspection). L'approche du revenu n'est pas appliquée (propriété à vocation principalement résidentielle ; " +231 "données de loyers non disponibles dans les sources publiques utilisées).", y);232233 /* --------------------- PAGE 3 : DESCRIPTION DE LA PROPRIÉTÉ --------------------- */234 y = newPage(c, TOTAL);235 y = sectionTitle(c, "3.", "Description de la propriété", y);236 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("3.1 Bâtiment", M, y); y += 16;237 y = kvTable(c, [238 ["Année de construction", u.anneeConstruction ? `${u.anneeConstruction}${s.anneeEstimee === "E" ? " (estimée au rôle)" : " (réelle)"}` : "—"],239 ["Aire d'étages (habitable)", u.aireEtagesM2 ? `${u.aireEtagesM2} m² (${Math.round(u.aireEtagesM2 * 10.7639)} pi²)` : "—"],240 ["Nombre d'étages", s.nbEtages != null ? String(s.nbEtages) : "—"],241 ["Genre de construction", s.genreConstruction ?? "—"],242 ["Lien physique", s.lienPhysique ?? "—"],243 ["Nombre de logements", u.nbLogements != null ? String(u.nbLogements) : "—"],244 ["Locaux non résidentiels", s.nbLocauxNonResid != null ? String(s.nbLocauxNonResid) : "—"],245 ], y);246 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("3.2 Terrain", M, y); y += 16;247 y = kvTable(c, [248 ["Superficie du terrain", u.superficieTerrainM2 ? `${u.superficieTerrainM2} m² (${Math.round(u.superficieTerrainM2 * 10.7639)} pi²)` : "—"],249 ["Mesure frontale", s.frontTerrainM ? `${s.frontTerrainM} m` : "—"],250 ["Unité de voisinage (rôle)", s.uniteVoisinage || "—"],251 ["Arrondissement", s.arrond || "—"],252 ], y);253 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("3.3 Données du rôle d'évaluation", M, y); y += 16;254 y = kvTable(c, [255 ["Valeur du terrain (rôle 2026)", fmt(s.valeurTerrain)],256 ["Valeur du bâtiment (rôle 2026)", fmt(s.valeurBatiment)],257 ["Valeur totale (rôle 2026)", fmt(u.valeurRole)],258 ["Valeur au rôle antérieur", fmt(s.valeurAnterieure)],259 ["Conditions du marché considérées au", s.datCondMarche ?? "—"],260 ], y);261 y = para(c,262 "Les caractéristiques ci-dessus proviennent du rôle d'évaluation foncière en vigueur (source : MAMH, données ouvertes). " +263 "Elles n'ont pas fait l'objet d'une vérification sur place.", y);264265 /* --------------------- PAGE 4 : GRILLE DES COMPARABLES --------------------- */266 y = newPage(c, TOTAL);267 y = sectionTitle(c, "4.", "Approche par comparaison — grille d'ajustement", y);268 const comps = r.comps.slice(0, 4);269 const colW = (CW - 118) / comps.length;270 const rowsG: [string, (ci: number) => string][] = [271 ["Adresse", (i) => `${comps[i].street ?? ""}`],272 ["Municipalité", (i) => comps[i].city ?? "—"],273 ["Date de vente", (i) => comps[i].date],274 ["Prix de vente", (i) => fmt(comps[i].amount)],275 ["Distance du sujet", (i) => comps[i].distanceM < 1000 ? `${Math.round(comps[i].distanceM)} m` : `${(comps[i].distanceM / 1000).toFixed(1)} km`],276 ["Aire d'étages", (i) => comps[i].floorArea ? `${comps[i].floorArea} m²` : "—"],277 ["Année constr.", (i) => comps[i].yearBuilt ? String(comps[i].yearBuilt) : "—"],278 ["Ajust. conditions de marché", (i) => signed(comps[i].adjTime)],279 ["Ajust. superficie", (i) => signed(comps[i].adjArea)],280 ["Ajust. âge", (i) => signed(comps[i].adjAge)],281 ["PRIX AJUSTÉ", (i) => fmt(Math.round(comps[i].adjustedPrice))],282 ["Pondération", (i) => `${(comps[i].weight * 100).toFixed(0)} %`],283 ];284 const rh = 20;285 // en-têtes colonnes286 doc.rect(M, y, 118, rh).fill(INK);287 doc.font("JB-Bold").fontSize(6.4).fillColor("#ffffff").text("ÉLÉMENT", M + 6, y + 7, { lineBreak: false });288 comps.forEach((_, i) => {289 doc.rect(M + 118 + i * colW, y, colW, rh).fillAndStroke(i % 2 ? SURFACE2 : "#ffffff", INK);290 doc.font("JB-Bold").fontSize(6.6).fillColor(GREEN_DEEP)291 .text(`COMPARABLE ${i + 1}`, M + 118 + i * colW + 6, y + 7, { lineBreak: false });292 });293 y += rh;294 rowsG.forEach(([label, get]) => {295 const bold = label === "PRIX AJUSTÉ";296 doc.rect(M, y, 118, rh).fillAndStroke(bold ? LIME : SURFACE2, INK);297 doc.font("JB-Bold").fontSize(6).fillColor(INK)298 .text(label.toUpperCase(), M + 6, y + 7, { width: 108, height: 8, ellipsis: true, lineBreak: false });299 comps.forEach((_, ci) => {300 doc.rect(M + 118 + ci * colW, y, colW, rh).fillAndStroke(bold ? "#f6fbe4" : "#ffffff", INK);301 doc.font(bold ? "SG-Bold" : "Inter").fontSize(bold ? 8.4 : 7.4).fillColor(bold ? INK : INK2)302 .text(get(ci), M + 118 + ci * colW + 6, y + (bold ? 5.5 : 6.5), { width: colW - 12, height: 10, ellipsis: true, lineBreak: false });303 });304 y += rh;305 });306 y += 10;307 y = para(c,308 `${r.nCompsUsed} ventes comparables ont été retenues au total (les ${comps.length} plus pondérées sont détaillées ci-dessus ; la liste ` +309 "complète figure dans le rapport standard). Chaque prix de vente est ajusté : (i) aux conditions actuelles du marché au moyen " +310 "d'un indice mensuel du prix médian au mètre carré du segment, (ii) pour l'écart de superficie habitable à raison de 50 % du " +311 "prix unitaire au m² du comparable, plafonné à ±25 %, et (iii) pour l'écart d'âge à raison de 0,5 % par année, plafonné à ±10 %. " +312 `La médiane pondérée des prix ajustés s'établit à ${fmt(r.compsEstimate)}.`, y);313314 // mini-indice de marché315 if (marketIndex.length > 3) {316 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("4.1 Indice des conditions de marché du segment", M, y);317 y += 16;318 const gw = CW, gh = 84;319 doc.roundedRect(M, y, gw, gh, 5).lineWidth(1).fillAndStroke("#ffffff", INK);320 const vals = marketIndex.map((p) => p.idx);321 const mn = Math.min(...vals), mx = Math.max(...vals), span = mx - mn || 1;322 const pts = marketIndex.map((p, i) => [323 M + 14 + (i / (marketIndex.length - 1)) * (gw - 28),324 y + gh - 18 - ((p.idx - mn) / span) * (gh - 36),325 ]);326 doc.moveTo(pts[0][0], pts[0][1]);327 pts.slice(1).forEach(([px, py]) => doc.lineTo(px, py));328 doc.lineWidth(1.8).stroke(GREEN);329 const last = pts[pts.length - 1];330 doc.circle(last[0], last[1], 3).lineWidth(1.2).fillAndStroke(LIME, INK);331 doc.font("JB-Reg").fontSize(6).fillColor(INK3)332 .text(marketIndex[0].month, M + 14, y + gh - 12, { lineBreak: false })333 .text(marketIndex[marketIndex.length - 1].month, M + gw - 60, y + gh - 12, { width: 46, align: "right" });334 doc.font("JB-Reg").fontSize(6).fillColor(INK3)335 .text(`BASE 1,00 = ${marketIndex[marketIndex.length - 1].month} · MÉDIANE $/M² LISSÉE 3 MOIS`, M + 14, y + 8, { characterSpacing: 0.5 });336 }337338 /* ------------------- PAGE 5 : HÉDONIQUE + RÉCONCILIATION ------------------- */339 y = newPage(c, TOTAL);340 y = sectionTitle(c, "5.", "Analyse hédonique de masse", y);341 y = para(c,342 "Un modèle de gradient boosting (LightGBM) est entraîné sur environ 690 000 transactions québécoises (2021-2026) appariées " +343 "au rôle d'évaluation foncière géoréférencé. Dix-huit variables décrivent chaque propriété (dimensions, âge, usage, " +344 "localisation fine, valeurs au rôle, temps). Sur un échantillon réservé de 102 943 ventes jamais vues du modèle, l'erreur " +345 "médiane est de 11,0 % et 72 % des estimations tombent à ±20 % du prix réel (ratio médian 0,995, sans biais systématique — " +346 "validation selon la norme IAAO sur études de ratios). L'incertitude propre à la propriété est quantifiée par deux modèles de " +347 "régression quantile (10e et 90e centiles).", y);348 y = kvTable(c, [349 ["Estimation du modèle hédonique", fmt(r.modelEstimate)],350 ["Intervalle P10-P90 du modèle", `${fmt(r.low)} → ${fmt(r.high)} (recentré)`],351 ["Erreur médiane du modèle (validation)", "11,0 % (MdAPE, holdout aléatoire)"],352 ], y);353354 y = sectionTitle(c, "6.", "Réconciliation et conclusion", y + 4);355 const rh2 = 20;356 const recRows: [string, string, string][] = [357 ["Approche", "Indication de valeur", "Pondération"],358 ["Modèle hédonique de masse", fmt(r.modelEstimate), `${(r.modelWeight * 100).toFixed(0)} %`],359 ["Comparaison directe (médiane pondérée)", fmt(r.compsEstimate), `${((1 - r.modelWeight) * 100).toFixed(0)} %`],360 ["VALEUR CONCLUE", fmt(r.estimate), "100 %"],361 ];362 recRows.forEach(([a, b, cc], i) => {363 const head = i === 0;364 const last = i === recRows.length - 1;365 doc.rect(M, y, CW, rh2).fillAndStroke(head ? INK : last ? LIME : i % 2 ? SURFACE2 : "#ffffff", INK);366 doc.font(head || last ? "JB-Bold" : "Inter").fontSize(head ? 6.8 : last ? 8.6 : 8)367 .fillColor(head ? "#ffffff" : INK)368 .text(head ? a.toUpperCase() : a, M + 8, y + 6, { lineBreak: false });369 doc.text(b, M + CW * 0.52, y + 6, { lineBreak: false });370 doc.text(cc, M + CW * 0.84, y + 6, { lineBreak: false });371 y += rh2;372 });373 y += 10;374 y = para(c,375 `Compte tenu de la qualité de l'appariement des données, du nombre et de la cohérence des comparables (dispersion ±${r.compsDispersionPct ?? "—"} %) ` +376 `et de la performance validée du modèle, la valeur marchande estimée de la propriété au ${generated} est conclue à ` +377 `${fmt(r.estimate)}, à l'intérieur d'une fourchette raisonnable de ${fmt(r.low)} à ${fmt(r.high)}. Indice de fiabilité : ` +378 `${r.confidenceLevel} (${r.confidencePct} %) — ${CONF_TXT[r.confidenceLevel]}.`, y);379380 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("6.1 Évolution de la valeur estimée (2021-2026)", M, y);381 y += 16;382 const hist = u.history.filter((h) => h.value != null);383 y = kvTable(c, hist.map((h) => [String(h.year), fmt(h.value)] as [string, string]), y, CW * 0.6);384385 /* ------------------- PAGE 6 : CONDITIONS + CERTIFICATION ------------------- */386 y = newPage(c, TOTAL);387 y = sectionTitle(c, "7.", "Hypothèses et conditions limitatives", y);388 const conds = [389 "Le présent rapport est généré automatiquement par le moteur d'évaluation statistique Vrai-Prix (modèle d'évaluation de masse, AVM). Il ne constitue pas un rapport d'évaluation au sens des normes de pratique professionnelle de l'Ordre des évaluateurs agréés du Québec (OEAQ) et ne peut être présenté comme tel.",390 "Aucune inspection physique de la propriété n'a été effectuée. L'état d'entretien, la qualité des finis, les rénovations, les dépendances et tout vice apparent ou caché ne sont pas considérés.",391 "Les caractéristiques utilisées proviennent du rôle d'évaluation foncière (MAMH, données ouvertes) et de transactions publiées. Leur exactitude et leur mise à jour ne sont pas garanties.",392 "Le droit évalué est présumé être la pleine propriété, libre de toute hypothèque, servitude, charge ou restriction inhabituelle. Aucune vérification de titres n'a été réalisée.",393 "La conformité aux lois, règlements de zonage et permis est présumée. Aucune étude environnementale n'a été effectuée ; la propriété est présumée exempte de contamination.",394 "La valeur conclue vaut à la date d'effet uniquement. Les marchés immobiliers évoluent ; l'estimation ne doit pas être utilisée à une autre date.",395 "La fourchette P10-P90 et l'indice de fiabilité font partie intégrante de la conclusion et doivent être communiqués avec elle.",396 "Ce rapport est destiné au destinataire indiqué en page titre, aux fins d'information et d'appui à une démarche de financement. Toute autre utilisation ou distribution engage la seule responsabilité de l'utilisateur.",397 "Pour une décision de financement finale, une confirmation par un évaluateur agréé (OEAQ) mandaté par l'institution financière est recommandée.",398 ];399 conds.forEach((txt, i) => {400 doc.font("JB-Bold").fontSize(7).fillColor(GREEN).text(`7.${i + 1}`, M, y + 1, { lineBreak: false });401 doc.font("Inter").fontSize(7.9).fillColor(INK2)402 .text(txt, M + 26, y, { width: CW - 26, lineGap: 1.6, align: "justify" });403 y = doc.y + 6;404 });405406 y = sectionTitle(c, "8.", "Attestation", y + 2);407 y = para(c,408 "J'atteste que les données, méthodes et pondérations décrites dans ce rapport sont celles réellement employées par le moteur " +409 "Vrai-Prix pour produire la conclusion de valeur ; que la méthodologie complète, ses métriques de validation et ses limites " +410 "sont publiées à vrai-prix.com/methodologie ; et que ni Vrai-Prix ni son exploitant n'ont d'intérêt présent ou futur dans la " +411 "propriété évaluée, ni de lien de dépendance avec les parties à une éventuelle transaction.", y);412 y += 14;413 doc.rect(M, y + 26, 200, 0.9).fill(INK);414 doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Vrai-Prix", M, y + 32);415 doc.font("Inter").fontSize(8).fillColor(INK2)416 .text("Simon-Pierre Boucher · contact@spboucher.ai", M, y + 46)417 .text(`Dossier ${dossier} · ${generated}`, M, y + 58);418 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)419 .text("ANNEXE DISPONIBLE : RAPPORT STANDARD (PLAN DES COMPARABLES, LISTE COMPLÈTE DES VENTES) — VRAI-PRIX.COM", M, y + 82, { characterSpacing: 0.5 });420421 doc.end();422 return done;423}424