diff --git a/backend/tests/test_zone_reason.py b/backend/tests/test_zone_reason.py
index 77c4f47..31f3c1d 100644
--- a/backend/tests/test_zone_reason.py
+++ b/backend/tests/test_zone_reason.py
@@ -69,4 +69,21 @@ class TestZoneReason:
r1 = _build_zone_reason(zone, case)
r2 = _build_zone_reason(dict(zone), dict(case))
assert r1 == r2
- assert 'совпадает с направлением' in r1
\ No newline at end of file
+ assert 'совпадает с направлением' in r1
+class TestRadiusConsistency:
+ def test_summary_radius_matches_max_times_multiplier(self):
+ """Радиус в summary = max_distance × психотип-множитель (согласован с хедером)."""
+ import asyncio
+ from services.rules_analysis import rules_analyze
+ case = {
+ 'age': 9, 'gender': 'м', 'season': 'лето', 'elapsed_hours': 4.0,
+ 'profiles': ['гармоничный'], 'psychotype': 'harmonic',
+ 'psychotype_modifiers': {'zone_0_500': 1.0, 'zone_500_1500': 1.0,
+ 'zone_1500_2500': 1.1, 'zone_2500plus': 1.0},
+ 'max_distance_km': 2.4,
+ 'lat': 54.5, 'lon': 28.5,
+ }
+ result = asyncio.run(rules_analyze(case))
+ # Средний психотип-множитель полос (1.0+1.0+1.1+1.0)/4 = 1.025 → 2.4×1.025=2.46→2.5
+ assert result.search_radius_km == 2.5, result.search_radius_km
+ assert f'{result.search_radius_km} км' in result.summary
diff --git a/frontend/src/pages/analysis/AnalysisResult.jsx b/frontend/src/pages/analysis/AnalysisResult.jsx
index 3b28ea1..bf91591 100644
--- a/frontend/src/pages/analysis/AnalysisResult.jsx
+++ b/frontend/src/pages/analysis/AnalysisResult.jsx
@@ -199,7 +199,10 @@ const AnalysisResult = () => {
{analysisData?.max_distance_km} км
{analysisData?.distance_multiplier !== 1 && (
- ×{analysisData?.distance_multiplier}
+ ×{analysisData?.distance_multiplier}
)}
diff --git a/services/rules_analysis.py b/services/rules_analysis.py
index efc20d1..ebb695d 100644
--- a/services/rules_analysis.py
+++ b/services/rules_analysis.py
@@ -178,6 +178,17 @@ def _psychotype_phrase(case_data: dict, distance_km: float) -> str | None:
return None
+def _psychotype_radius_multiplier(case_data: dict) -> float:
+ """Средний множитель психотипа по полосам дистанции (без двойного учёта
+ возраст/сезон — они уже в max_distance)."""
+ mods = case_data.get('psychotype_modifiers')
+ if not mods:
+ return 1.0
+ vals = [mods.get(k, 1.0) for k in ('zone_0_500', 'zone_500_1500',
+ 'zone_1500_2500', 'zone_2500plus')]
+ return sum(vals) / len(vals)
+
+
def _build_zone_reason(zone: dict, case_data: dict) -> str:
"""Словесное пояснение приоритета зоны — по тем же факторам, что считает
WeightedScorer (детерминированно, без выдумок): рельеф, вода, дороги, НП,
@@ -238,7 +249,8 @@ async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult:
urgency = _age_urgency(age, profiles)
primary_zones: list[PrimaryZone] = []
- search_radius_km = 5.0
+ base_km = float(case_data.get('max_distance_km') or 5.0)
+ search_radius_km = round(base_km * 1.0, 1)
if case_data.get('lat') and case_data.get('lon'):
try:
@@ -273,7 +285,11 @@ async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult:
distance=zone['distance_km'],
reason=_build_zone_reason(zone, case_data),
))
- search_radius_km = scorer.distance_multiplier * 2.0
+ # Радиус = max_distance (НормС×коэффициенты) × средний психотип-множитель.
+ # score_zone.distance_multiplier НЕ используется: он накопил age/season,
+ # уже учтённые в max_distance — двойной учёт.
+ base_km = float(case_data.get('max_distance_km') or 2.0)
+ search_radius_km = round(base_km * _psychotype_radius_multiplier(case_data), 1)
except Exception as e:
logger.warning(f'Geo/scoring service error: {e}')
diff --git a/services/search_engine.py b/services/search_engine.py
index 3b844a2..b6f1c4f 100644
--- a/services/search_engine.py
+++ b/services/search_engine.py
@@ -269,6 +269,7 @@ async def build_search_model(
case_data['psychotype_modifiers'] = psychotype_modifiers
max_distance_km = calculate_max_distance(case_data)
+ case_data['max_distance_km'] = max_distance_km
analysis = await analyzer(case_data)
scorer = WeightedScorer()