feat(mera/b2c): в карточке игры схема улицы сделки, а не пятно района

Карта показывала ПОЛИГОН РАЙОНА — десятки квадратных километров, на которых
одинаково выглядят сделка у парка и сделка у ТЭЦ. Схема улиц уже приезжает
строкой витрины (`street_scheme`), рисовать её было нечем.

`StreetMapV3` — серверный компонент, инлайновый <svg>, НИ ОДНОГО внешнего
запроса: тайлы отдали бы провайдеру IP посетителя и то, что он смотрит, на
странице, которая обещает «без звонков и регистрации» (правило шапки
layout.tsx, по нему же в дереве self-hosted шрифты). Слои снизу вверх: плашка
и приглушённая сетка (уже были), вода, фоновые дороги толщиной по классу OSM,
целевая улица акцентом поверх мягкого ореола, подписи соседних улиц
моноширинным.

DealMapV3 ОСТАЁТСЯ и не тронут. Улица матчится у 92.1% строк витрины, у
остальных схемы нет — там по-прежнему район. Ветвление стоит в GuessGameV3,
чтобы обе карты остались тупыми: каждая рисует то, что ей дали, и ни одна не
подставляет вместо отсутствующих данных правдоподобное.

ЧТО КАРТА ГОВОРИТ О СЕБЕ. Подпись — «<УЛИЦА> · ДОМ НЕ ИЗВЕСТЕН». Адрес сделки
уровня улицы, номер дома известен у 2.7% сделок, координата окна — центроид
улицы. Слов «объект» и «адрес» в карточке нет; тест держит и то и другое.
Точку дома поставить нечем и по построению: в `street_scheme` нет ни констант
проекции, ни координат окна.

ЧЕГО НЕ РИСУЕМ. Зданий: `cad_buildings` — 18 307 контуров на город, в плотном
центре 51 здание на радиус 450 м, где их в разы больше; нарисованная застройка
заявляла бы полноту, которой нет. Улицы — фильтрованная выгрузка «источников
шума», дворовых и служебных проездов в ней нет, поэтому фоновые дороги
приглушены: это контекст, а не план квартала.

КАДР — `slice`, а не `meet`: окно квадратное, карточка нет, и `meet` оставил бы
поля по бокам. Обрезка безопасна ровно потому, что целевая улица лежит в ЦЕНТРЕ
окна по построению; обрезаются края с частью подписей соседей.

ДОСТУПНОСТЬ — как у DealMapV3, `aria-hidden`: всё, что схема сообщает, стоит
рядом текстом. `role="img"` заставил бы прочитать то же самое дважды, а
названия соседних улиц без их взаимного расположения ничего не значат.

ЦВЕТА — токенами: `--b2c-water` (вода отдельным оттенком, иначе склеивается с
дорогами) и `--b2c-muted-dark` (подписи на тёмной плашке: `muted` даёт там
3.2:1). Литералов в CSS не добавлено.

Подпись объекта лежит ПОВЕРХ карты, и карта теперь доходит до краёв — добавлено
затемнение под ней и плашка под подписью карты; без них белый текст пересекался
с подсвеченной улицей. Псевдоэлемент позиционирован, поэтому детям
`.gameMapBottom` задан `position: relative` — иначе затемнение красится поверх
текста и стирает его (поймано скриншотом, а не рассуждением).

Тесты (каждый сломан вручную и покраснел): кадр из данных и `slice`; ореол шире
линии и по одному на путь; иерархия дорог и незнакомый класс не дают нулевой
толщины; вода отделена; подпись называет улицу и говорит про дом; `aria-hidden`;
ветвление карточки в обе стороны.

Проверено глазами на dev-сервере, стаб — реальные строки прода плюс схемы,
собранные той же проекцией по геометрии gendesign: улица сматчилась; не
сматчилась (виден район); полей схемы нет вовсе (тот же район, без падения).
This commit is contained in:
bot-backend 2026-08-29 22:41:07 +05:00
parent ca3f073b0e
commit a5ca2a326a
9 changed files with 356 additions and 11 deletions

View file

@ -13,9 +13,11 @@ import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { DealMapV3 } from "../_components/v3/DealMapV3";
import { dealTitle } from "../_components/v3/deal-view";
import { GuessGameV3 } from "../_components/v3/GuessGameV3";
import { StreetMapV3 } from "../_components/v3/StreetMapV3";
import { EKB_MAP } from "../_components/v3/ekb-districts";
import type { ShowcaseDeal } from "../public-api";
import { dealTitle, toGameRound } from "../_components/v3/deal-view";
import type { ShowcaseDeal, StreetScheme } from "../public-api";
/** Ширина кадра из атрибута viewBox отрисованной карты. */
function frameWidth(container: HTMLElement): number {
@ -108,6 +110,8 @@ describe("подпись сделки различает студию и ком
note: "",
lat: null,
lon: null,
street_name: null,
street_scheme: null,
});
it("rooms = 0 подписывается студией, а не «0-к»", () => {
@ -121,3 +125,104 @@ describe("подпись сделки различает студию и ком
expect(dealTitle(deal(2, 52))).not.toContain("Студия");
});
});
// ── Схема улиц: что она обязана сказать и чего не должна заявлять ───────────
//
// Схема приходит готовыми путями из ручки — рисовать тут нечего, и «рисуется
// ли svg» проверять бессмысленно. Ломаются другие вещи, и каждая ломается
// тихо: пропадает иерархия дорог (схема превращается в решётку), теряется
// подсветка целевой улицы (карта перестаёт быть про эту сделку), подпись
// начинает выдавать улицу за адрес объекта, а выбор между двумя картами
// съезжает — и 8% строк без схемы получают пустоту вместо района.
const SCHEME: StreetScheme = {
street: "улица Краснолесья",
w: 1000,
h: 1000,
target: ["M431.2 88L500.1 210.4", "M500.1 210.4L512 640"],
roads: [
{ c: "trunk", d: "M0 0L100 100" },
{ c: "residential", d: "M0 900L100 950" },
{ c: "unknown_class", d: "M10 10L20 20" },
],
water: ["M0 500L1000 520"],
labels: [{ t: "улица Чкалова", x: 431.2, y: 88 }],
};
const widths = (container: HTMLElement, cls: string): number[] =>
[...container.querySelectorAll(`path[class*="${cls}"]`)].map((n) =>
Number(n.getAttribute("stroke-width")),
);
describe("схема улиц в карточке сделки", () => {
it("кадр берётся из данных, а не из константы фронта", () => {
const { container } = render(<StreetMapV3 scheme={{ ...SCHEME, w: 800, h: 800 }} />);
expect(container.querySelector("svg")!.getAttribute("viewBox")).toBe("0 0 800 800");
// slice, а не meet: окно квадратное, карточка — нет. Целевая улица лежит в
// центре окна по построению, поэтому обрезка краёв её не трогает.
expect(container.querySelector("svg")!.getAttribute("preserveAspectRatio")).toContain("slice");
});
it("целевая улица подсвечена и несёт ореол — по одному на каждый путь", () => {
const { container } = render(<StreetMapV3 scheme={SCHEME} />);
expect(widths(container, "streetTarget")).toHaveLength(SCHEME.target.length);
const halo = widths(container, "streetHalo");
expect(halo).toHaveLength(SCHEME.target.length);
// Ореол обязан быть ШИРЕ линии — иначе он не ореол, а вторая линия.
expect(Math.min(...halo)).toBeGreaterThan(Math.max(...widths(container, "streetTarget")));
});
it("иерархия дорог сохраняется, а незнакомый класс не даёт нулевой толщины", () => {
const { container } = render(<StreetMapV3 scheme={SCHEME} />);
const [trunk, residential, unknown] = widths(container, "streetRoad");
expect(trunk).toBeGreaterThan(residential);
expect(unknown).toBeGreaterThan(0);
});
it("вода нарисована и отделена от дорог", () => {
const { container } = render(<StreetMapV3 scheme={SCHEME} />);
expect(widths(container, "streetWater")).toHaveLength(SCHEME.water.length);
});
it("подпись называет улицу и сразу говорит, что дом не известен", () => {
const { container, getByText } = render(<StreetMapV3 scheme={SCHEME} />);
expect(getByText(/УЛИЦА КРАСНОЛЕСЬЯ · ДОМ НЕ ИЗВЕСТЕН/u)).toBeTruthy();
expect(container.textContent).not.toMatch(/АДРЕС|ОБЪЕКТ/u);
});
it("схема ничего не сообщает скринридеру — всё, что она знает, стоит рядом текстом", () => {
const { container } = render(<StreetMapV3 scheme={SCHEME} />);
expect(container.querySelector("svg")!.getAttribute("aria-hidden")).toBe("true");
});
});
describe("карточка игры выбирает карту по наличию схемы", () => {
const deal = (scheme: StreetScheme | null): ShowcaseDeal => ({
district: "Ленинский",
rooms: 2,
area_m2: 52,
floor: 3,
total_floors: 9,
deal_quarter: "II кв. 2026",
predicted_rub: 5_210_000,
fact_rub: 5_180_000,
err_pct: 0.58,
n_analogs: 12,
note: "",
lat: null,
lon: null,
street_name: scheme ? scheme.street : null,
street_scheme: scheme,
});
it("схема есть — показана улица", () => {
const { container } = render(<GuessGameV3 rounds={[toGameRound(deal(SCHEME), 0)]} />);
expect(container.textContent).toContain("ДОМ НЕ ИЗВЕСТЕН");
});
it("схемы нет — остаётся район, а не пустое место", () => {
const { container } = render(<GuessGameV3 rounds={[toGameRound(deal(null), 0)]} />);
expect(container.textContent).toContain("ЛЕНИНСКИЙ");
expect(container.textContent).not.toContain("ДОМ НЕ ИЗВЕСТЕН");
});
});

View file

@ -56,6 +56,9 @@ const SHOWCASE: ShowcaseResponse = {
note: "Прогноз посчитан по активным объявлениям на дату пересчёта",
lat: null,
lon: null,
// Схемы у этой строки нет — рендер идёт запасным путём, картой района.
street_name: null,
street_scheme: null,
},
],
stats: {

View file

@ -10,13 +10,18 @@
* реальных сделках» была неправдой. Сделок мало игру не рендерит родитель:
* играть на одной строке под заголовком «три квартиры» нельзя.
*
* КАРТА (`DealMapV3`) настоящая: границы районов ЕКБ статикой, кадр по
* району сделки, точка по координате, когда она есть. Раньше здесь стоял
* декоративный прямоугольник с подписью «ОБЪЕКТ · РАЙОН», а координат в
* витрине не было вовсе. Подпись объекта на карте сменилась: координата это
* ЦЕНТРОИД УЛИЦЫ, а не дом, и карта произносит это сама (разбор в шапке
* DealMapV3). Ниже карты объект по-прежнему подписан только тем, что известно:
* комнаты, площадь, этаж, район, квартал сделки. Адреса среди них нет.
* КАРТА настоящая, и их две. Есть схема улиц (`street_scheme`, 92.1% строк
* витрины) рисуется `StreetMapV3`: окрестности улицы сделки. Нет остаётся
* `DealMapV3` с полигоном района, как было. Выбор делается ЗДЕСЬ, чтобы обе
* карты оставались тупыми: каждая рисует то, что ей дали, и ни одна не
* подставляет вместо отсутствующих данных правдоподобное.
*
* Обе подписывают себя сами и обе говорят про масштаб: схема улиц «улица ·
* ДОМ НЕ ИЗВЕСТЕН», район «СЕРЕДИНА УЛИЦЫ, НЕ ДОМ». Раньше тут стоял
* декоративный прямоугольник с подписью «ОБЪЕКТ · РАЙОН», заявлявший
* местоположение объекта то, чего в данных нет. Ниже карты объект
* по-прежнему подписан только тем, что известно: комнаты, площадь, этаж,
* район, квартал сделки. Адреса среди них нет.
*
* Ошибка игрока считается медианой |ответ факт| / факт по сыгранным раундам
* той же формулой, что и ошибка МЕРЫ на этих же сделках (`err_pct` витрины).
@ -29,6 +34,7 @@ import { useState } from "react";
import styles from "../../landing-v3.module.css";
import type { GameRound } from "./deal-view";
import { DealMapV3 } from "./DealMapV3";
import { StreetMapV3 } from "./StreetMapV3";
import { PublicLink } from "../PublicLink";
@ -99,7 +105,11 @@ export function GuessGameV3({ rounds }: { rounds: readonly GameRound[] }) {
<div className={styles.gameGrid}>
<div className={styles.gameMap}>
<div className={styles.gameMapGrid} aria-hidden="true" />
<DealMapV3 district={r.district} districtLabel={r.districtLabel} lat={r.lat} lon={r.lon} />
{r.streetScheme ? (
<StreetMapV3 scheme={r.streetScheme} />
) : (
<DealMapV3 district={r.district} districtLabel={r.districtLabel} lat={r.lat} lon={r.lon} />
)}
<div className={styles.gameMapBottom}>
<div className={styles.gameAddr}>{r.title}</div>
<div className={styles.gameMeta}>{r.meta}</div>

View file

@ -0,0 +1,112 @@
/**
* StreetMapV3 схема улицы сделки в карточке игры.
*
* ЧТО ЭТО ЗАМЕНЯЕТ. `DealMapV3` показывал полигон РАЙОНА единственную
* геометрию, до которой у tradein был доступ. Район это десятки квадратных
* километров; на нём одинаково выглядят сделка у парка и сделка у ТЭЦ. С
* 29.08.2026 строка витрины несёт готовую схему улиц (`street_scheme`), и
* карточка показывает окрестности улицы, а не пятно в четверть города.
*
* DealMapV3 ОСТАЁТСЯ. Адрес сделки нормализуется и матчится с названиями OSM у
* 550 названий из 654, что покрывает 92.1% строк витрины (замер на проде
* 29.08.2026). У остальных схемы нет там по-прежнему район. Выбор делает
* родитель: схема есть эта карта, нет та.
*
* КАРТА ГОВОРИТ, ЧТО ОНА ПОКАЗЫВАЕТ. Подпись «<улица> · ДОМ НЕ ИЗВЕСТЕН».
* Адрес сделки в ДКП-данных уровня улицы, номер дома известен у 2.7% сделок, и
* координата окна центроид улицы. Точки дома тут нет и быть не может: в
* `street_scheme` НАМЕРЕННО нет ни констант проекции, ни координат окна
* поставить точку по этой схеме нельзя даже по ошибке. Поэтому карточка не
* произносит слов «объект» и «адрес»: она показывает улицу и так и говорит.
*
* ЗДАНИЙ НЕТ. `cad_buildings` 18 307 контуров на город; в плотном центре это
* 51 здание на радиус 450 м, где их в разы больше. Нарисованная застройка
* заявляла бы полноту, которой в данных нет, и пустые кварталы читались бы как
* пустыри. Улицы тоже фильтрованная выгрузка («источники шума»): именованные
* покрыты хорошо, дворовые и служебные проезды отсутствуют, поэтому фоновые
* дороги нарисованы приглушённо как контекст, а не как план квартала.
*
* НИ ОДНОГО ВНЕШНЕГО ЗАПРОСА, как и у соседа: тайлы отдали бы провайдеру IP
* посетителя и то, что он смотрит, на странице, которая обещает «без звонков и
* регистрации» (то же правило, по которому в дереве self-hosted шрифты см.
* шапку layout.tsx). Всё, что нужно, уже приехало строкой витрины.
*
* КАДР. `slice`, а не `meet`: окно квадратное, карточка нет, и `meet`
* оставил бы поля по бокам, а карта на плашке читается как карта, только когда
* доходит до краёв. Обрезать безопасно ровно потому, что целевая улица лежит в
* ЦЕНТРЕ окна по построению (окно строится вокруг её центроида) при любой
* пропорции карточки центр остаётся в кадре. Обрезаются края: часть подписей
* соседних улиц. Они контекст, а не содержание.
*
* ДОСТУПНОСТЬ как у DealMapV3, `aria-hidden`. Всё, что схема сообщает
* какая это улица и что дом не известен, стоит рядом текстом, подписью под
* картой. `role="img"` с `aria-label` заставил бы скринридер прочитать то же
* самое дважды подряд; названий соседних улиц он не сообщит всё равно
* геометрию словами не передать, а список из семи улиц без их взаимного
* расположения ничего не значит.
*
* Компонент чистый и без состояния "use client" в нём нет. В клиентский
* бандл он попадает вместе с карточкой игры, которая листает раунды.
*/
import type { StreetScheme } from "../../public-api";
import styles from "../../landing-v3.module.css";
/**
* Толщина фоновых дорог по классу, в единицах кадра (окно 840 м = 1000
* единиц). Иерархия магистраль проезд, а не одна линия на всё: без неё
* схема превращается в решётку, на которой не видно, куда улица ведёт.
*/
const ROAD_W: Record<string, number> = {
trunk: 7,
primary: 6,
secondary: 4.6,
tertiary: 3.4,
residential: 2.2,
};
const ROAD_W_FALLBACK = 2.2;
/** Целевая улица: акцент поверх мягкого ореола той же формы. */
const TARGET_W = 6;
const HALO_W = 18;
/** Вода одной толщиной: ручей и река различаются шириной, которой у нас нет. */
const WATER_W = 5;
const LABEL_SIZE = 21;
export function StreetMapV3({ scheme }: { readonly scheme: StreetScheme }) {
return (
<>
<svg
className={styles.gameMapSvg}
viewBox={`0 0 ${scheme.w} ${scheme.h}`}
preserveAspectRatio="xMidYMid slice"
aria-hidden="true"
focusable="false"
>
{scheme.water.map((d, i) => (
<path key={`w${i}`} d={d} className={styles.streetWater} strokeWidth={WATER_W} />
))}
{scheme.roads.map((r, i) => (
<path
key={`r${i}`}
d={r.d}
className={styles.streetRoad}
strokeWidth={ROAD_W[r.c] ?? ROAD_W_FALLBACK}
/>
))}
{scheme.target.map((d, i) => (
<path key={`h${i}`} d={d} className={styles.streetHalo} strokeWidth={HALO_W} />
))}
{scheme.target.map((d, i) => (
<path key={`t${i}`} d={d} className={styles.streetTarget} strokeWidth={TARGET_W} />
))}
{scheme.labels.map((l) => (
<text key={l.t} x={l.x} y={l.y} className={styles.streetLabel} fontSize={LABEL_SIZE}>
{l.t}
</text>
))}
</svg>
<div className={styles.gameMapLabel}>{`${scheme.street.toUpperCase()} · ДОМ НЕ ИЗВЕСТЕН`}</div>
</>
);
}

View file

@ -12,7 +12,7 @@
* координата уезжает только в карту, а в текстовые склейки нет.
*/
import type { ShowcaseDeal } from "../../public-api";
import type { ShowcaseDeal, StreetScheme } from "../../public-api";
const RUB = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 });
const PCT = new Intl.NumberFormat("ru-RU", {
@ -74,6 +74,12 @@ export interface GameRound {
/** Центроид УЛИЦЫ сделки, не дом (см. `note` строки витрины). */
readonly lat: number | null;
readonly lon: number | null;
/**
* Готовая схема улиц вокруг центра улицы сделки либо `null` у тех строк,
* чей адрес не сматчился с OSM. Раунду хватает одного поля: район в нём уже
* есть и остаётся запасным вариантом карты, подставлять вместо схемы нечего.
*/
readonly streetScheme: StreetScheme | null;
readonly quarter: string;
/** Границы слайдера, млн ₽. */
readonly min: number;
@ -112,6 +118,7 @@ export function toGameRound(deal: ShowcaseDeal, index: number): GameRound {
district: deal.district,
lat: deal.lat,
lon: deal.lon,
streetScheme: deal.street_scheme,
quarter: deal.deal_quarter,
min,
max,

View file

@ -55,6 +55,21 @@ export const b2c = {
*/
lineDark: "#2C3A3F",
/**
* Приглушённый текст НА ТЁМНОЙ плашке. `muted` (#667579) на `surfaceDark`
* даёт 3.2:1 и для подписей не годится; этот 6.5:1. Заведён под подписи
* соседних улиц на схеме карты сделки.
*/
mutedDark: "#8C9CA1",
/**
* Вода на схеме улиц (реки, каналы, ручьи, пруды). Отдельный оттенок, а не
* `line-dark`: вода и дорога разные вещи, и одинаковая линия склеила бы
* их в одну сетку. Синий увод от акцента, чтобы вода не читалась как
* подсвеченная улица.
*/
water: "#2A5A78",
/** Акцент — заливки/обводки/декор. НЕ для текста, см. шапку. */
accent: "#16A9BC",
/** Акцент для заливки кнопок (под белым текстом). */

View file

@ -1674,8 +1674,52 @@
fill: var(--b2c-accent);
stroke: var(--b2c-surface-dark);
}
/* --- Схема улиц (StreetMapV3). Порядок в разметке = порядок слоёв. --- */
/* Вода: под дорогами, своим оттенком — иначе сливается с сеткой улиц. */
.streetWater {
fill: none;
stroke: var(--b2c-water);
stroke-linecap: round;
stroke-linejoin: round;
}
/* Фоновые дороги контекст. Выгрузка неполна (нет дворовых проездов),
поэтому линия приглушена: это не план квартала. `line-dark` на плашке
почти не виден берём приглушённый текстовый оттенок и гасим прозрачностью,
чтобы сеть читалась, но не спорила с подсвеченной улицей. */
.streetRoad {
fill: none;
stroke: var(--b2c-muted-dark);
stroke-opacity: 0.45;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Ореол целевой улицы — та же форма, шире и почти прозрачно. */
.streetHalo {
fill: none;
stroke: var(--b2c-accent);
stroke-opacity: 0.18;
stroke-linecap: round;
stroke-linejoin: round;
}
.streetTarget {
fill: none;
stroke: var(--b2c-accent);
stroke-linecap: round;
stroke-linejoin: round;
}
.streetLabel {
fill: var(--b2c-muted-dark);
font-family: var(--b2c-font-mono);
letter-spacing: 0.04em;
}
.gameMapLabel {
position: relative;
/* Плашка под подписью. Карта доходит до краёв, и подпись ложится прямо на
улицу на акцентной линии акцентный же текст не читается вовсе. */
align-self: flex-start;
padding: 5px 9px;
border-radius: 4px;
background: var(--b2c-surface-dark);
font-family: var(--b2c-font-mono);
font-size: 9.5px;
letter-spacing: 0.14em;
@ -1684,6 +1728,20 @@
.gameMapBottom {
position: relative;
}
/* Затемнение под подписью объекта: она лежит ПОВЕРХ карты, и без него белый
текст пересекается с подсвеченной улицей. Растягивается за padding плашки,
чтобы не было видно шва. */
.gameMapBottom::before {
content: "";
position: absolute;
inset: -46px calc(-1 * clamp(24px, 3vw, 34px)) calc(-1 * clamp(24px, 3vw, 34px));
background: linear-gradient(to bottom, transparent, var(--b2c-surface-dark) 45%);
}
/* Затемнение ПОЗИЦИОНИРОВАННЫЙ псевдоэлемент, а сам текст нет: без этой
строки он красится ПОВЕРХ подписи и стирает её. */
.gameMapBottom > * {
position: relative;
}
.gameAddr {
font-weight: 800;
font-size: clamp(19px, 2.2vw, 24px);

View file

@ -194,6 +194,31 @@ export interface LandingStat {
*/
export type LandingStats = Readonly<Record<string, LandingStat | undefined>>;
/**
* Схема улиц вокруг центра улицы сделки уже спроецированные SVG-пути.
* Приходит готовой из `/showcase`: на фронте геометрии нет, есть картинка.
*
* ПОЛЕЙ ПРОЕКЦИИ И КООРДИНАТ ОКНА ЗДЕСЬ НЕТ НАМЕРЕННО. По этой схеме нельзя
* поставить точку дома а дом известен у 2.7% сделок, и ставить её было бы
* нечем и незачем. Это замок, а не забывчивость: см. шапку
* `backend/app/services/street_scheme.py`.
*/
export interface StreetScheme {
/** Название улицы как в OSM — дубль `street_name`. */
street: string;
/** Размеры квадратного окна viewBox; `h` всегда равно `w`. */
w: number;
h: number;
/** Подсвеченная улица сделки. Непуста, если схема вообще есть. */
target: string[];
/** Фоновые дороги: `c` — класс OSM (trunk…residential). */
roads: { c: string; d: string }[];
/** Реки, каналы, ручьи, пруды. Пруды приходят замкнутыми кольцами. */
water: string[];
/** Подписи соседних улиц, не более семи. */
labels: { t: string; x: number; y: number }[];
}
export interface ShowcaseDeal {
district: string | null;
rooms: number;
@ -213,6 +238,14 @@ export interface ShowcaseDeal {
*/
lat: number | null;
lon: number | null;
/**
* Улица сделки, как она названа в OSM. `null` примерно у 8% строк адрес
* не сматчился с OSM; это штатный случай, а не сбой, и подставлять на его
* место район нельзя: район не улица.
*/
street_name: string | null;
/** Схема улиц. `null` ровно тогда же, когда `street_name`. */
street_scheme: StreetScheme | null;
}
/** Подпись под витриной: из чего отобраны показанные строки. */

View file

@ -95,6 +95,8 @@ export const b2cVars = {
"--b2c-ink": b2c.ink,
"--b2c-muted": b2c.muted,
"--b2c-muted-dark": b2c.mutedDark,
"--b2c-water": b2c.water,
"--b2c-line": b2c.line,
"--b2c-line-soft": b2c.lineSoft,