tkadkghdlf commited on
Commit
e4eee80
·
verified ·
1 Parent(s): 8e47459

Sync DemoService aa0ec3b35f9dd33dbf0d56e23f5af0ad1cac70f4

Browse files
api/ai-service/app/api/v1/endpoints/route_generation.py CHANGED
@@ -51,6 +51,7 @@ class RouteGenerateRequest(BaseModel):
51
  selectedLabel: str | None = None
52
  selectedTopK: int = Field(default=0, ge=0, le=20)
53
  fastRoads: bool = True
 
54
  generationId: str | None = Field(default=None, max_length=200)
55
  # The route-search page knows the centre of every selectable region. Keep
56
  # it as a recovery coordinate so a temporary geocoder failure never turns
@@ -402,6 +403,7 @@ def _run_fast_road_pipeline(
402
  route_count: int,
403
  target_distance_km: float,
404
  selected_label: str | None,
 
405
  ) -> dict[str, Any]:
406
  """Create real walkable-road routes without per-route CLIP inference.
407
 
@@ -425,12 +427,17 @@ def _run_fast_road_pipeline(
425
  # failed request before it can return any usable road loop.
426
  examples = getattr(module, "GPX_SHAPE_TEMPLATES", {}).get(selected_label, [])
427
  candidate_pool = min(16 if examples else 8, max(route_count, route_count + (11 if examples else 1)))
428
- candidates = build_candidates(
429
  graph=graph,
430
  route_count=candidate_pool,
431
  target_distance_m=target_distance_km * 1000.0,
432
  selected_label=selected_label,
433
  )
 
 
 
 
 
434
  # A selected shape is a preference, not a reason to leave the runner with
435
  # no route. Sparse neighbourhoods and long distances do not always have
436
  # enough roads to satisfy the strict shape-quality gate. In that case
@@ -438,7 +445,7 @@ def _run_fast_road_pipeline(
438
  # the response marks them as non-art-ready so the UI can state this
439
  # honestly instead of showing a red generation failure.
440
  generation_mode = "fast_walk_roads"
441
- if not candidates and selected_label:
442
  candidates = build_candidates(
443
  graph=graph,
444
  route_count=candidate_pool,
@@ -447,7 +454,7 @@ def _run_fast_road_pipeline(
447
  tolerance_ratio=0.60,
448
  )
449
  generation_mode = "fast_walk_roads_nearby_fallback"
450
- if not candidates:
451
  # Rural/island road graphs may have no acceptable drawing or loop.
452
  # A return trip along existing walking edges is still a real route.
453
  candidates = _nearby_road_return_routes(
@@ -502,6 +509,9 @@ def _run_fast_road_pipeline(
502
  abs(route["length_km"] - target_distance_km),
503
  ))
504
  routes = routes[:route_count]
 
 
 
505
  if not routes:
506
  raise RuntimeError("Walkable road routes did not contain usable map coordinates.")
507
 
@@ -511,7 +521,8 @@ def _run_fast_road_pipeline(
511
  "candidate_count": len(candidates),
512
  "requested_count": route_count,
513
  "generated_count": len(routes),
514
- "target_distance_km": target_distance_km,
 
515
  "radius_km": radius_km,
516
  "labels": [selected_label] if selected_label else [],
517
  "selected_label": selected_label,
@@ -673,12 +684,12 @@ def generate_routes(payload: RouteGenerateRequest) -> dict[str, Any]:
673
 
674
  route_count_cap = 20 if use_current else 100
675
  route_count = min(route_count_cap, max(1, payload.routeCount))
676
- target_distance_km = max(0.5, payload.targetDistanceKm)
677
  # A 1.5 km graph is enough for ordinary short courses, but it cannot make
678
  # a truthful 16~30 km route. Grow the road graph only as much as the
679
  # requested distance needs, up to the request model's safe 10 km limit.
680
  required_radius_km = target_distance_km / 5.5
681
- radius_km = min(10.0, max(1.5, min(10.0, payload.radiusKm), required_radius_km))
682
 
683
  if payload.fastRoads:
684
  module = None
@@ -693,6 +704,7 @@ def generate_routes(payload: RouteGenerateRequest) -> dict[str, Any]:
693
  route_count=route_count,
694
  target_distance_km=target_distance_km,
695
  selected_label=selected_label,
 
696
  )
697
  except Exception as primary_exc:
698
  logger.exception("fast walk-road route generation failed")
@@ -722,22 +734,36 @@ def generate_routes(payload: RouteGenerateRequest) -> dict[str, Any]:
722
  route_count=route_count,
723
  target_distance_km=target_distance_km,
724
  selected_label=selected_label,
 
725
  )
726
  except Exception:
727
  logger.exception("fallback-centre walk-road generation failed")
728
  # Provider outages must reach this branch too. Previously the
729
  # early skip above also skipped recovery and returned HTTP 503.
730
  template_fn = getattr(module, "guided_waypoints_for_label", None)
 
 
 
 
 
 
 
731
  real_roads = generate_walking_fallback(
732
  center_lat=fallback_lat, center_lon=fallback_lon,
733
  route_count=route_count, target_distance_km=target_distance_km,
734
  selected_label=selected_label, place=payload.place,
735
- template=template_fn(selected_label) if callable(template_fn) else None,
736
- templates=getattr(module, "GPX_SHAPE_TEMPLATES", {}).get(selected_label, []),
737
  generation_id=payload.generationId,
 
738
  )
739
  if real_roads:
740
  return real_roads
 
 
 
 
 
741
  return _coordinate_fallback_routes(
742
  center_lat=fallback_lat,
743
  center_lon=fallback_lon,
 
51
  selectedLabel: str | None = None
52
  selectedTopK: int = Field(default=0, ge=0, le=20)
53
  fastRoads: bool = True
54
+ autoDistance: bool = False
55
  generationId: str | None = Field(default=None, max_length=200)
56
  # The route-search page knows the centre of every selectable region. Keep
57
  # it as a recovery coordinate so a temporary geocoder failure never turns
 
403
  route_count: int,
404
  target_distance_km: float,
405
  selected_label: str | None,
406
+ auto_distance: bool = False,
407
  ) -> dict[str, Any]:
408
  """Create real walkable-road routes without per-route CLIP inference.
409
 
 
427
  # failed request before it can return any usable road loop.
428
  examples = getattr(module, "GPX_SHAPE_TEMPLATES", {}).get(selected_label, [])
429
  candidate_pool = min(16 if examples else 8, max(route_count, route_count + (11 if examples else 1)))
430
+ candidate_options = dict(
431
  graph=graph,
432
  route_count=candidate_pool,
433
  target_distance_m=target_distance_km * 1000.0,
434
  selected_label=selected_label,
435
  )
436
+ if auto_distance:
437
+ candidate_options["auto_distance"] = True
438
+ candidates = build_candidates(**candidate_options)
439
+ if auto_distance and selected_label:
440
+ candidates = [route for route in candidates if route.get("generation_mode") == "label_guided"]
441
  # A selected shape is a preference, not a reason to leave the runner with
442
  # no route. Sparse neighbourhoods and long distances do not always have
443
  # enough roads to satisfy the strict shape-quality gate. In that case
 
445
  # the response marks them as non-art-ready so the UI can state this
446
  # honestly instead of showing a red generation failure.
447
  generation_mode = "fast_walk_roads"
448
+ if not candidates and selected_label and not auto_distance:
449
  candidates = build_candidates(
450
  graph=graph,
451
  route_count=candidate_pool,
 
454
  tolerance_ratio=0.60,
455
  )
456
  generation_mode = "fast_walk_roads_nearby_fallback"
457
+ if not candidates and not auto_distance:
458
  # Rural/island road graphs may have no acceptable drawing or loop.
459
  # A return trip along existing walking edges is still a real route.
460
  candidates = _nearby_road_return_routes(
 
509
  abs(route["length_km"] - target_distance_km),
510
  ))
511
  routes = routes[:route_count]
512
+ elif auto_distance:
513
+ routes.sort(key=lambda route: -route["top1_score"])
514
+ routes = routes[:route_count]
515
  if not routes:
516
  raise RuntimeError("Walkable road routes did not contain usable map coordinates.")
517
 
 
521
  "candidate_count": len(candidates),
522
  "requested_count": route_count,
523
  "generated_count": len(routes),
524
+ "target_distance_km": routes[0]["length_km"] if auto_distance else target_distance_km,
525
+ "auto_distance": auto_distance,
526
  "radius_km": radius_km,
527
  "labels": [selected_label] if selected_label else [],
528
  "selected_label": selected_label,
 
684
 
685
  route_count_cap = 20 if use_current else 100
686
  route_count = min(route_count_cap, max(1, payload.routeCount))
687
+ target_distance_km = 6.0 if payload.autoDistance else max(0.5, payload.targetDistanceKm)
688
  # A 1.5 km graph is enough for ordinary short courses, but it cannot make
689
  # a truthful 16~30 km route. Grow the road graph only as much as the
690
  # requested distance needs, up to the request model's safe 10 km limit.
691
  required_radius_km = target_distance_km / 5.5
692
+ radius_km = min(10.0, max(5.0 if payload.autoDistance else 1.5, min(10.0, payload.radiusKm), required_radius_km))
693
 
694
  if payload.fastRoads:
695
  module = None
 
704
  route_count=route_count,
705
  target_distance_km=target_distance_km,
706
  selected_label=selected_label,
707
+ auto_distance=payload.autoDistance,
708
  )
709
  except Exception as primary_exc:
710
  logger.exception("fast walk-road route generation failed")
 
734
  route_count=route_count,
735
  target_distance_km=target_distance_km,
736
  selected_label=selected_label,
737
+ auto_distance=payload.autoDistance,
738
  )
739
  except Exception:
740
  logger.exception("fallback-centre walk-road generation failed")
741
  # Provider outages must reach this branch too. Previously the
742
  # early skip above also skipped recovery and returned HTTP 503.
743
  template_fn = getattr(module, "guided_waypoints_for_label", None)
744
+ guided_template = template_fn(selected_label) if callable(template_fn) else None
745
+ gpx_templates = getattr(module, "GPX_SHAPE_TEMPLATES", {}).get(selected_label, [])
746
+ if payload.autoDistance and selected_label and not guided_template and not gpx_templates:
747
+ raise HTTPException(
748
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
749
+ detail="선택한 모양의 경로 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해주세요.",
750
+ )
751
  real_roads = generate_walking_fallback(
752
  center_lat=fallback_lat, center_lon=fallback_lon,
753
  route_count=route_count, target_distance_km=target_distance_km,
754
  selected_label=selected_label, place=payload.place,
755
+ template=guided_template,
756
+ templates=gpx_templates,
757
  generation_id=payload.generationId,
758
+ auto_distance=payload.autoDistance,
759
  )
760
  if real_roads:
761
  return real_roads
762
+ if payload.autoDistance:
763
+ raise HTTPException(
764
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
765
+ detail="선택한 지역에서 해당 모양에 가까운 실제 보행 경로를 찾지 못했습니다. 다시 찾기를 눌러주세요.",
766
+ )
767
  return _coordinate_fallback_routes(
768
  center_lat=fallback_lat,
769
  center_lon=fallback_lon,
api/ai-service/app/services/walking_route_fallback.py CHANGED
@@ -80,8 +80,9 @@ def generate_walking_fallback(
80
  template: list[tuple[float, float]] | None = None,
81
  templates: list[list[tuple[float, float]]] | None = None,
82
  generation_id: str | None = None,
 
83
  ) -> dict[str, Any] | None:
84
- key = (round(center_lat, 5), round(center_lon, 5), route_count, target_distance_km, selected_label, generation_id)
85
  with _LOCK:
86
  cached = _CACHE.get(key)
87
  if cached and time.monotonic() - cached[0] < 6 * 3600:
@@ -110,10 +111,13 @@ def generate_walking_fallback(
110
  perimeter = sum(math.hypot(b[0]-a[0], b[1]-a[1]) for a, b in zip(outline, outline[1:]))
111
  if perimeter <= 0:
112
  continue
113
- scale = target_distance_km * 1000 / perimeter / 1.2
 
 
 
114
  angle = generation_angle(generation_id) + attempt * math.pi * (3 - math.sqrt(5))
115
  # Shorter retries also work near a coast or on a small island.
116
- size = scale * (1 if attempt < 3 else 0.5 if attempt < 5 else 0.2)
117
  locations = []
118
  for x, y in outline:
119
  dx, dy = (x-outline[0][0]) * size, (y-outline[0][1]) * size
@@ -131,7 +135,7 @@ def generate_walking_fallback(
131
  trip = response.json().get("trip", {})
132
  summary = trip.get("summary", {})
133
  length = float(summary.get("length", 0))
134
- if summary.get("has_ferry") or not math.isfinite(length) or length <= 0:
135
  continue
136
  coords = []
137
  for leg in trip.get("legs", []):
@@ -165,11 +169,12 @@ def generate_walking_fallback(
165
  if not routes:
166
  return None
167
  routes.sort(key=lambda route: (-route["route_quality"].get("gpx_similarity", 0.0),
168
- abs(route["length_km"]-target_distance_km)))
169
  routes = routes[:route_count]
170
  result = {"place":place or f"{center_lat:.6f},{center_lon:.6f}","source":"point",
171
  "candidate_count":len(routes),"requested_count":route_count,"generated_count":len(routes),
172
- "target_distance_km":target_distance_km,"labels":[selected_label] if selected_label else [],
 
173
  "selected_label":selected_label,"selected_top_k":0,"routes":routes,"best_route":routes[0],
174
  "generation_mode":"independent_walk_roads",
175
  "fallback_notice":"주변 실제 보행 도로를 이용해 경로를 생성했습니다. 도로 구조에 따라 선택한 모양과 목표 거리는 달라질 수 있습니다."}
 
80
  template: list[tuple[float, float]] | None = None,
81
  templates: list[list[tuple[float, float]]] | None = None,
82
  generation_id: str | None = None,
83
+ auto_distance: bool = False,
84
  ) -> dict[str, Any] | None:
85
+ key = (round(center_lat, 5), round(center_lon, 5), route_count, target_distance_km, selected_label, generation_id, auto_distance)
86
  with _LOCK:
87
  cached = _CACHE.get(key)
88
  if cached and time.monotonic() - cached[0] < 6 * 3600:
 
111
  perimeter = sum(math.hypot(b[0]-a[0], b[1]-a[1]) for a, b in zip(outline, outline[1:]))
112
  if perimeter <= 0:
113
  continue
114
+ # Auto mode searches several sizes of the same GPX drawing. The
115
+ # result's measured length, rather than a hidden default, is shown.
116
+ requested_size_km = (3, 5, 8, 12, 16, 20, 6, 10)[attempt] if auto_distance else target_distance_km
117
+ scale = requested_size_km * 1000 / perimeter / 1.2
118
  angle = generation_angle(generation_id) + attempt * math.pi * (3 - math.sqrt(5))
119
  # Shorter retries also work near a coast or on a small island.
120
+ size = scale if auto_distance else scale * (1 if attempt < 3 else 0.5 if attempt < 5 else 0.2)
121
  locations = []
122
  for x, y in outline:
123
  dx, dy = (x-outline[0][0]) * size, (y-outline[0][1]) * size
 
135
  trip = response.json().get("trip", {})
136
  summary = trip.get("summary", {})
137
  length = float(summary.get("length", 0))
138
+ if summary.get("has_ferry") or not math.isfinite(length) or length <= 0 or (auto_distance and not 1.5 <= length <= 20):
139
  continue
140
  coords = []
141
  for leg in trip.get("legs", []):
 
169
  if not routes:
170
  return None
171
  routes.sort(key=lambda route: (-route["route_quality"].get("gpx_similarity", 0.0),
172
+ 0 if auto_distance else abs(route["length_km"]-target_distance_km)))
173
  routes = routes[:route_count]
174
  result = {"place":place or f"{center_lat:.6f},{center_lon:.6f}","source":"point",
175
  "candidate_count":len(routes),"requested_count":route_count,"generated_count":len(routes),
176
+ "target_distance_km":routes[0]["length_km"] if auto_distance else target_distance_km,
177
+ "auto_distance":auto_distance,"labels":[selected_label] if selected_label else [],
178
  "selected_label":selected_label,"selected_top_k":0,"routes":routes,"best_route":routes[0],
179
  "generation_mode":"independent_walk_roads",
180
  "fallback_notice":"주변 실제 보행 도로를 이용해 경로를 생성했습니다. 도로 구조에 따라 선택한 모양과 목표 거리는 달라질 수 있습니다."}
api/ai-service/tests/test_route_generation_recovery.py CHANGED
@@ -113,6 +113,29 @@ def test_real_independent_roads_are_preferred_to_estimated_preview(pipeline):
113
  assert result == expected
114
 
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  def test_gpx_guided_fast_routes_are_ranked_by_actual_road_similarity(monkeypatch):
117
  candidates = [
118
  {"nodes": [1, 2], "length_m": 5900, "shape_quality": 0.8, "is_loop": True},
 
113
  assert result == expected
114
 
115
 
116
+ def test_auto_distance_passes_through_to_real_road_fallback(pipeline):
117
+ pipeline.setattr(routes, "_load_pipeline_module", lambda _: SimpleNamespace(
118
+ guided_waypoints_for_label=lambda _label: [(0, 0), (1, 0), (1, 1), (0, 0)]))
119
+ pipeline.setattr(routes, "_run_fast_road_pipeline", Mock(side_effect=RuntimeError("All Overpass endpoints failed")))
120
+ expected = {"generation_mode": "independent_walk_roads", "routes": [{"estimated_route": False}]}
121
+ walking = Mock(return_value=expected)
122
+ pipeline.setattr(routes, "generate_walking_fallback", walking)
123
+ result = routes.generate_routes(routes.RouteGenerateRequest(
124
+ source="current", latitude=37.5, longitude=127, selectedLabel="dog", labels=["dog"], autoDistance=True))
125
+ assert result == expected
126
+ assert walking.call_args.kwargs["auto_distance"] is True
127
+
128
+
129
+ def test_auto_distance_never_returns_estimated_coordinates(pipeline):
130
+ pipeline.setattr(routes, "_load_pipeline_module", lambda _: SimpleNamespace(
131
+ guided_waypoints_for_label=lambda _label: [(0, 0), (1, 0), (1, 1), (0, 0)]))
132
+ pipeline.setattr(routes, "_run_fast_road_pipeline", Mock(side_effect=RuntimeError("All Overpass endpoints failed")))
133
+ with pytest.raises(routes.HTTPException) as error:
134
+ routes.generate_routes(routes.RouteGenerateRequest(
135
+ source="current", latitude=37.5, longitude=127, selectedLabel="dog", labels=["dog"], autoDistance=True))
136
+ assert error.value.status_code == 503
137
+
138
+
139
  def test_gpx_guided_fast_routes_are_ranked_by_actual_road_similarity(monkeypatch):
140
  candidates = [
141
  {"nodes": [1, 2], "length_m": 5900, "shape_quality": 0.8, "is_loop": True},
api/ai-service/tests/test_walking_route_fallback.py CHANGED
@@ -139,3 +139,21 @@ def test_gpx_fallback_checks_multiple_examples_and_returns_best_match(monkeypatc
139
  assert all(location['radius'] == 400 for locations_in_request in locations for location in locations_in_request)
140
  assert result['best_route']['route_quality']['gpx_similarity'] == 0.9
141
  assert result['generated_count'] == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  assert all(location['radius'] == 400 for locations_in_request in locations for location in locations_in_request)
140
  assert result['best_route']['route_quality']['gpx_similarity'] == 0.9
141
  assert result['generated_count'] == 1
142
+
143
+
144
+ def test_auto_distance_tries_multiple_sizes_and_reports_measured_length(monkeypatch):
145
+ import json
146
+ response_obj = SimpleNamespace(raise_for_status=lambda: None,
147
+ json=lambda: {'trip': {'summary': {'length': 4.2, 'has_ferry': False},
148
+ 'legs': [{'shape': encode([[37.5, 127.0], [37.501, 127.002], [37.5, 127.0]])}]}})
149
+ get = Mock(return_value=response_obj)
150
+ monkeypatch.setattr(requests, 'get', get)
151
+ examples = [[(0, 0), (1, 0), (1, 1), (0, 0)]]
152
+ result = walking.generate_walking_fallback(**options(), templates=examples,
153
+ generation_id='auto-sizes', auto_distance=True)
154
+ assert result['target_distance_km'] == 4.2
155
+ assert result['auto_distance'] is True
156
+ assert get.call_count == 4
157
+ first = json.loads(get.call_args_list[0].kwargs['params']['json'])['locations']
158
+ second = json.loads(get.call_args_list[1].kwargs['params']['json'])['locations']
159
+ assert first[1] != second[1]
main/app/route-results/[id]/page.tsx CHANGED
@@ -148,7 +148,7 @@ function GeneratedRouteDetailPageContent() {
148
  source: "place",
149
  label: record.routeLabel,
150
  routeCount: "8",
151
- targetDistanceKm: String(Math.max(1, Math.round(record.route.length_km))),
152
  selectedTopK: "8",
153
  radiusKm: "2.5",
154
  });
 
148
  source: "place",
149
  label: record.routeLabel,
150
  routeCount: "8",
151
+ autoDistance: "1",
152
  selectedTopK: "8",
153
  radiusKm: "2.5",
154
  });
main/app/route-results/page.tsx CHANGED
@@ -140,7 +140,6 @@ function RouteResultsPageContent() {
140
  const initialLongitude = parseNumberParam(searchParams.get("longitude"));
141
  const initialRadiusKm = parseNumberParam(searchParams.get("radiusKm"));
142
  const initialCount = parseNumberParam(searchParams.get("routeCount"));
143
- const initialDistance = parseNumberParam(searchParams.get("targetDistanceKm"));
144
  const initialSelectedTopK = parseNumberParam(searchParams.get("selectedTopK"));
145
  const fallbackLatitude = parseNumberParam(searchParams.get("fallbackLatitude"));
146
  const fallbackLongitude = parseNumberParam(searchParams.get("fallbackLongitude"));
@@ -148,7 +147,6 @@ function RouteResultsPageContent() {
148
 
149
  const [place, setPlace] = useState(initialPlace);
150
  const [routeCountInput, setRouteCountInput] = useState(Number.isFinite(initialCount) ? String(initialCount) : "8");
151
- const [targetDistanceInput, setTargetDistanceInput] = useState(Number.isFinite(initialDistance) ? String(initialDistance) : "5");
152
  const [loading, setLoading] = useState(false);
153
  const [error, setError] = useState<string | null>(null);
154
  const [routeQualityNotice, setRouteQualityNotice] = useState<string | null>(null);
@@ -168,16 +166,15 @@ function RouteResultsPageContent() {
168
  const regenerateRoutes = () => {
169
  const requestPlace = place.trim();
170
  const count = Number(routeCountInput);
171
- const distance = Number(targetDistanceInput);
172
- if (!requestPlace || !Number.isFinite(count) || count < 5 || count > 100 ||
173
- !Number.isFinite(distance) || distance < 1 || distance > 30) {
174
  void fetchRoutes(); // Keep the same field validation messages.
175
  return;
176
  }
177
  const params = new URLSearchParams(searchParams.toString());
178
  params.set("place", requestPlace);
179
  params.set("routeCount", String(count));
180
- params.set("targetDistanceKm", String(distance));
 
181
  if (requestPlace !== initialPlace.trim()) {
182
  params.set("source", "place");
183
  for (const key of ["latitude", "longitude", "fallbackLatitude", "fallbackLongitude"]) params.delete(key);
@@ -192,7 +189,6 @@ function RouteResultsPageContent() {
192
 
193
  const fetchRoutes = async () => {
194
  const routeCount = Number(routeCountInput);
195
- const targetDistanceKm = Number(targetDistanceInput);
196
  const requestPlace = place.trim();
197
  const placeWasEdited = requestPlace !== initialPlace.trim();
198
 
@@ -205,10 +201,6 @@ function RouteResultsPageContent() {
205
  setError("경로 수는 5~100 사이로 입력해주세요.");
206
  return;
207
  }
208
- if (!Number.isFinite(targetDistanceKm) || targetDistanceKm < 1 || targetDistanceKm > 30) {
209
- setError("목표 거리는 1~30km 사이로 입력해주세요.");
210
- return;
211
- }
212
 
213
  activeRequest.current?.abort();
214
  const controller = new AbortController();
@@ -250,7 +242,7 @@ function RouteResultsPageContent() {
250
  longitude: placeWasEdited ? null : longitude,
251
  radiusKm,
252
  routeCount,
253
- targetDistanceKm,
254
  labels: AVAILABLE_LABELS,
255
  selectedLabel,
256
  selectedTopK,
@@ -331,8 +323,7 @@ function RouteResultsPageContent() {
331
  // Store under the request's own key. A separate effect used to write
332
  // the previous result into the newly selected location's cache.
333
  try {
334
- if (!placeWasEdited && routeCount === (Number.isFinite(initialCount) ? initialCount : 8)
335
- && targetDistanceKm === (Number.isFinite(initialDistance) ? initialDistance : 5)) {
336
  window.sessionStorage.setItem(cacheKey, JSON.stringify(parsed));
337
  }
338
  } catch {
@@ -480,7 +471,7 @@ function RouteResultsPageContent() {
480
  />
481
 
482
  <SectionCard>
483
- <div className="grid gap-3 sm:grid-cols-[1fr_120px_120px] sm:items-end">
484
  <label className="grid gap-2">
485
  <span className="rts-label">지역</span>
486
  <input value={place} onChange={(event) => setPlace(event.target.value)} className="rts-input" />
@@ -489,10 +480,6 @@ function RouteResultsPageContent() {
489
  <span className="rts-label">경로 수</span>
490
  <input type="number" min={5} max={100} value={routeCountInput} onChange={(event) => setRouteCountInput(event.target.value)} className="rts-input" />
491
  </label>
492
- <label className="grid gap-2">
493
- <span className="rts-label">목표 거리</span>
494
- <input type="number" min={1} max={30} step={0.5} value={targetDistanceInput} onChange={(event) => setTargetDistanceInput(event.target.value)} className="rts-input" />
495
- </label>
496
  </div>
497
 
498
  {error ? <p className="mt-3 text-sm text-red-600">{error}</p> : null}
@@ -517,8 +504,8 @@ function RouteResultsPageContent() {
517
  <span>추천 경로</span>
518
  </div>
519
  <div>
520
- <strong>{formatDistance(result.target_distance_km)}</strong>
521
- <span>목표 거리</span>
522
  </div>
523
  <div>
524
  <strong>{formatGeneratedRouteLabel(selectedLabel)}</strong>
 
140
  const initialLongitude = parseNumberParam(searchParams.get("longitude"));
141
  const initialRadiusKm = parseNumberParam(searchParams.get("radiusKm"));
142
  const initialCount = parseNumberParam(searchParams.get("routeCount"));
 
143
  const initialSelectedTopK = parseNumberParam(searchParams.get("selectedTopK"));
144
  const fallbackLatitude = parseNumberParam(searchParams.get("fallbackLatitude"));
145
  const fallbackLongitude = parseNumberParam(searchParams.get("fallbackLongitude"));
 
147
 
148
  const [place, setPlace] = useState(initialPlace);
149
  const [routeCountInput, setRouteCountInput] = useState(Number.isFinite(initialCount) ? String(initialCount) : "8");
 
150
  const [loading, setLoading] = useState(false);
151
  const [error, setError] = useState<string | null>(null);
152
  const [routeQualityNotice, setRouteQualityNotice] = useState<string | null>(null);
 
166
  const regenerateRoutes = () => {
167
  const requestPlace = place.trim();
168
  const count = Number(routeCountInput);
169
+ if (!requestPlace || !Number.isFinite(count) || count < 5 || count > 100) {
 
 
170
  void fetchRoutes(); // Keep the same field validation messages.
171
  return;
172
  }
173
  const params = new URLSearchParams(searchParams.toString());
174
  params.set("place", requestPlace);
175
  params.set("routeCount", String(count));
176
+ params.delete("targetDistanceKm");
177
+ params.set("autoDistance", "1");
178
  if (requestPlace !== initialPlace.trim()) {
179
  params.set("source", "place");
180
  for (const key of ["latitude", "longitude", "fallbackLatitude", "fallbackLongitude"]) params.delete(key);
 
189
 
190
  const fetchRoutes = async () => {
191
  const routeCount = Number(routeCountInput);
 
192
  const requestPlace = place.trim();
193
  const placeWasEdited = requestPlace !== initialPlace.trim();
194
 
 
201
  setError("경로 수는 5~100 사이로 입력해주세요.");
202
  return;
203
  }
 
 
 
 
204
 
205
  activeRequest.current?.abort();
206
  const controller = new AbortController();
 
242
  longitude: placeWasEdited ? null : longitude,
243
  radiusKm,
244
  routeCount,
245
+ autoDistance: true,
246
  labels: AVAILABLE_LABELS,
247
  selectedLabel,
248
  selectedTopK,
 
323
  // Store under the request's own key. A separate effect used to write
324
  // the previous result into the newly selected location's cache.
325
  try {
326
+ if (!placeWasEdited && routeCount === (Number.isFinite(initialCount) ? initialCount : 8)) {
 
327
  window.sessionStorage.setItem(cacheKey, JSON.stringify(parsed));
328
  }
329
  } catch {
 
471
  />
472
 
473
  <SectionCard>
474
+ <div className="grid gap-3 sm:grid-cols-[1fr_120px] sm:items-end">
475
  <label className="grid gap-2">
476
  <span className="rts-label">지역</span>
477
  <input value={place} onChange={(event) => setPlace(event.target.value)} className="rts-input" />
 
480
  <span className="rts-label">경로 수</span>
481
  <input type="number" min={5} max={100} value={routeCountInput} onChange={(event) => setRouteCountInput(event.target.value)} className="rts-input" />
482
  </label>
 
 
 
 
483
  </div>
484
 
485
  {error ? <p className="mt-3 text-sm text-red-600">{error}</p> : null}
 
504
  <span>추천 경로</span>
505
  </div>
506
  <div>
507
+ <strong>{result.best_route ? formatDistance(result.best_route.length_km) : "-"}</strong>
508
+ <span>첫 번째 경로 거리</span>
509
  </div>
510
  <div>
511
  <strong>{formatGeneratedRouteLabel(selectedLabel)}</strong>
main/app/route-search/page.tsx CHANGED
@@ -499,13 +499,11 @@ function RouteSearchPageContent() {
499
  const [isRegionPickerOpen, setIsRegionPickerOpen] = useState(false);
500
  const [isDistrictPickerOpen, setIsDistrictPickerOpen] = useState(false);
501
  const [isShapePickerOpen, setIsShapePickerOpen] = useState(false);
502
- const [isDistancePickerOpen, setIsDistancePickerOpen] = useState(false);
503
  const [latitude, setLatitude] = useState<number | null>(null);
504
  const [longitude, setLongitude] = useState<number | null>(null);
505
  const [locationAccuracyM, setLocationAccuracyM] = useState<number | null>(null);
506
  const [radiusKm, setRadiusKm] = useState(2.5);
507
  const [routeCountInput, setRouteCountInput] = useState("5");
508
- const [targetDistanceInput, setTargetDistanceInput] = useState("6");
509
  const [selectedLabel, setSelectedLabel] = useState(requestedLabel || "cloud");
510
  const [geoError, setGeoError] = useState<string | null>(null);
511
  const [locationPermissionState, setLocationPermissionState] =
@@ -528,7 +526,6 @@ function RouteSearchPageContent() {
528
  );
529
  const districtOptions = activeRegion.districts;
530
  const routeCountPresets = [5, 8, 16];
531
- const distanceOptions = [3, 5, 7, 8, 10, 12, 16];
532
  const radiusPresets = [2.5, 5, 10];
533
  const labelOptions = useMemo(() => SHAPE_OPTIONS.map((shape) => shape.key), []);
534
  const selectedShape =
@@ -604,8 +601,8 @@ function RouteSearchPageContent() {
604
  : [];
605
  const isWaitingForCurrentLocation = isLocating;
606
  const summaryPlace = source === "current" ? "현재 위치" : selectedDistrictLabel;
607
- const selectedSummary = `${summaryPlace} · ${selectedShape.label} · ${targetDistanceInput || "-"}km`;
608
- const routePlanText = `${summaryPlace}에서 ${selectedShape.label} 모양에 가까운 ${targetDistanceInput || "-"}km 경로를 ${routeCountInput || "-"}개 추천합니다.`;
609
 
610
  useEffect(() => {
611
  const isValid = isUiFlowPermitValid(FLOW_KEYS.routeSearch, flowToken);
@@ -771,26 +768,17 @@ function RouteSearchPageContent() {
771
  }
772
 
773
  const routeCount = Number(routeCountInput);
774
- const targetDistanceKm = Number(targetDistanceInput);
775
  if (!Number.isFinite(routeCount) || routeCount < 5 || routeCount > 100) {
776
  setFormError("추천 경로 수는 5~100 사이 숫자로 입력해 주세요.");
777
  return;
778
  }
779
- if (
780
- !Number.isFinite(targetDistanceKm) ||
781
- targetDistanceKm < 1 ||
782
- targetDistanceKm > 30
783
- ) {
784
- setFormError("목표 거리는 1~30km 사이 숫자로 입력해 주세요.");
785
- return;
786
- }
787
  setIsNavigating(true);
788
  const params = new URLSearchParams({
789
  source: effectiveSource,
790
  place: effectivePlace,
791
  radiusKm: String(radiusKm),
792
  routeCount: String(routeCount),
793
- targetDistanceKm: String(targetDistanceKm),
794
  label: selectedLabel,
795
  selectedTopK: "3",
796
  // Supply the visible map centre for a server-side recovery attempt when
@@ -932,7 +920,6 @@ function RouteSearchPageContent() {
932
  setIsRegionPickerOpen((open) => !open);
933
  setIsDistrictPickerOpen(false);
934
  setIsShapePickerOpen(false);
935
- setIsDistancePickerOpen(false);
936
  }}
937
  className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
938
  >
@@ -988,7 +975,6 @@ function RouteSearchPageContent() {
988
  setIsDistrictPickerOpen((open) => !open);
989
  setIsRegionPickerOpen(false);
990
  setIsShapePickerOpen(false);
991
- setIsDistancePickerOpen(false);
992
  }}
993
  className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
994
  >
@@ -1060,8 +1046,8 @@ function RouteSearchPageContent() {
1060
  </div>
1061
  </section>
1062
 
1063
- <section className={`relative rounded-[28px] border border-black/10 bg-white p-5 shadow-[0_18px_45px_rgba(0,0,0,0.06)] ${isShapePickerOpen || isDistancePickerOpen ? "z-[2400]" : ""}`}>
1064
- <h2 className="text-xl font-bold">모양과 거리</h2>
1065
 
1066
  <div className="mt-4 grid gap-4">
1067
  <label className="grid gap-2">
@@ -1073,7 +1059,6 @@ function RouteSearchPageContent() {
1073
  setIsShapePickerOpen((open) => !open);
1074
  setIsRegionPickerOpen(false);
1075
  setIsDistrictPickerOpen(false);
1076
- setIsDistancePickerOpen(false);
1077
  }}
1078
  className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
1079
  >
@@ -1113,56 +1098,6 @@ function RouteSearchPageContent() {
1113
  </div>
1114
  </label>
1115
 
1116
- <label className="grid gap-2">
1117
- <span className="text-sm font-bold">목표 거리</span>
1118
- <div className="relative">
1119
- <button
1120
- type="button"
1121
- onClick={() => {
1122
- setIsDistancePickerOpen((open) => !open);
1123
- setIsRegionPickerOpen(false);
1124
- setIsDistrictPickerOpen(false);
1125
- setIsShapePickerOpen(false);
1126
- }}
1127
- className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
1128
- >
1129
- <span>{targetDistanceInput || "-"}km</span>
1130
- <span className="ml-3 text-xs text-[#666]">
1131
- {isDistancePickerOpen ? "▲" : "▼"}
1132
- </span>
1133
- </button>
1134
-
1135
- <MotionPopover
1136
- open={isDistancePickerOpen}
1137
- className="absolute left-0 right-0 top-[54px] z-[2300] max-h-[308px] overflow-y-auto rounded-2xl border border-black/10 bg-white py-2 shadow-[0_14px_34px_rgba(0,0,0,0.16)]"
1138
- >
1139
- {distanceOptions.map((distance) => {
1140
- const value = String(distance);
1141
- const isSelected = targetDistanceInput === value;
1142
-
1143
- return (
1144
- <button
1145
- key={distance}
1146
- type="button"
1147
- onMouseDown={(event) => event.preventDefault()}
1148
- onClick={() => {
1149
- setTargetDistanceInput(value);
1150
- setIsDistancePickerOpen(false);
1151
- }}
1152
- className={`flex h-11 w-full items-center px-4 text-left text-sm font-semibold ${
1153
- isSelected
1154
- ? "bg-[#1E40AF] text-white"
1155
- : "text-black hover:bg-[#f3f7ff]"
1156
- }`}
1157
- >
1158
- {distance}km
1159
- </button>
1160
- );
1161
- })}
1162
- </MotionPopover>
1163
- </div>
1164
- </label>
1165
-
1166
  <details className="rounded-2xl bg-[#f7f7f7] p-4">
1167
  <summary className="cursor-pointer text-sm font-bold">세부 설정</summary>
1168
  <div className="mt-4 grid gap-4">
 
499
  const [isRegionPickerOpen, setIsRegionPickerOpen] = useState(false);
500
  const [isDistrictPickerOpen, setIsDistrictPickerOpen] = useState(false);
501
  const [isShapePickerOpen, setIsShapePickerOpen] = useState(false);
 
502
  const [latitude, setLatitude] = useState<number | null>(null);
503
  const [longitude, setLongitude] = useState<number | null>(null);
504
  const [locationAccuracyM, setLocationAccuracyM] = useState<number | null>(null);
505
  const [radiusKm, setRadiusKm] = useState(2.5);
506
  const [routeCountInput, setRouteCountInput] = useState("5");
 
507
  const [selectedLabel, setSelectedLabel] = useState(requestedLabel || "cloud");
508
  const [geoError, setGeoError] = useState<string | null>(null);
509
  const [locationPermissionState, setLocationPermissionState] =
 
526
  );
527
  const districtOptions = activeRegion.districts;
528
  const routeCountPresets = [5, 8, 16];
 
529
  const radiusPresets = [2.5, 5, 10];
530
  const labelOptions = useMemo(() => SHAPE_OPTIONS.map((shape) => shape.key), []);
531
  const selectedShape =
 
601
  : [];
602
  const isWaitingForCurrentLocation = isLocating;
603
  const summaryPlace = source === "current" ? "현재 위치" : selectedDistrictLabel;
604
+ const selectedSummary = `${summaryPlace} · ${selectedShape.label} · 거리 자동`;
605
+ const routePlanText = `${summaryPlace}에서 목표 거리를 정하지 않고 ${selectedShape.label} 모양에 가까운 경로를 찾아 ${routeCountInput || "-"}개 추천합니다.`;
606
 
607
  useEffect(() => {
608
  const isValid = isUiFlowPermitValid(FLOW_KEYS.routeSearch, flowToken);
 
768
  }
769
 
770
  const routeCount = Number(routeCountInput);
 
771
  if (!Number.isFinite(routeCount) || routeCount < 5 || routeCount > 100) {
772
  setFormError("추천 경로 수는 5~100 사이 숫자로 입력해 주세요.");
773
  return;
774
  }
 
 
 
 
 
 
 
 
775
  setIsNavigating(true);
776
  const params = new URLSearchParams({
777
  source: effectiveSource,
778
  place: effectivePlace,
779
  radiusKm: String(radiusKm),
780
  routeCount: String(routeCount),
781
+ autoDistance: "1",
782
  label: selectedLabel,
783
  selectedTopK: "3",
784
  // Supply the visible map centre for a server-side recovery attempt when
 
920
  setIsRegionPickerOpen((open) => !open);
921
  setIsDistrictPickerOpen(false);
922
  setIsShapePickerOpen(false);
 
923
  }}
924
  className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
925
  >
 
975
  setIsDistrictPickerOpen((open) => !open);
976
  setIsRegionPickerOpen(false);
977
  setIsShapePickerOpen(false);
 
978
  }}
979
  className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
980
  >
 
1046
  </div>
1047
  </section>
1048
 
1049
+ <section className={`relative rounded-[28px] border border-black/10 bg-white p-5 shadow-[0_18px_45px_rgba(0,0,0,0.06)] ${isShapePickerOpen ? "z-[2400]" : ""}`}>
1050
+ <h2 className="text-xl font-bold">모양</h2>
1051
 
1052
  <div className="mt-4 grid gap-4">
1053
  <label className="grid gap-2">
 
1059
  setIsShapePickerOpen((open) => !open);
1060
  setIsRegionPickerOpen(false);
1061
  setIsDistrictPickerOpen(false);
 
1062
  }}
1063
  className="flex h-12 w-full items-center justify-between rounded-2xl border border-black/10 bg-[#f7f7f7] px-4 text-left text-sm font-semibold"
1064
  >
 
1098
  </div>
1099
  </label>
1100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1101
  <details className="rounded-2xl bg-[#f7f7f7] p-4">
1102
  <summary className="cursor-pointer text-sm font-bold">세부 설정</summary>
1103
  <div className="mt-4 grid gap-4">
router/ai/scripts/clip_osm_region_test.py CHANGED
@@ -2387,6 +2387,7 @@ def generate_candidate_routes(
2387
  target_distance_m: float,
2388
  tolerance_ratio: float = 0.35,
2389
  selected_label: str | None = None,
 
2390
  ) -> list[dict[str, Any]]:
2391
  nodes = list(graph.nodes)
2392
  if len(nodes) < 8:
@@ -2415,10 +2416,11 @@ def generate_candidate_routes(
2415
  shuffled_examples = random.sample(examples, len(examples)) if examples else []
2416
  for attempt in range(guided_attempts):
2417
  try:
 
2418
  geo_points = guided_geo_waypoints(
2419
  graph=graph,
2420
  label=str(selected_label),
2421
- target_distance_m=target_distance_m,
2422
  template=shuffled_examples[attempt % len(shuffled_examples)] if shuffled_examples else None,
2423
  )
2424
  waypoint_nodes = nearest_nodes_for_geo_points(graph, geo_points)
@@ -2429,7 +2431,7 @@ def generate_candidate_routes(
2429
  if len(route_nodes) < 18:
2430
  continue
2431
  length_m = route_length_m(graph, route_nodes)
2432
- if length_m <= 0:
2433
  continue
2434
  unique_ratio = len(set(route_nodes)) / len(route_nodes)
2435
  if unique_ratio < 0.50:
@@ -2481,10 +2483,20 @@ def generate_candidate_routes(
2481
  selected_label, shape_metrics, is_loop=True
2482
  ),
2483
  }
2484
- if delta <= tolerance:
2485
  accepted.append(row)
2486
  fallback.append(row)
2487
 
 
 
 
 
 
 
 
 
 
 
2488
  # 2) closed-loop random candidates
2489
  for _ in range(max_attempts):
2490
  try:
@@ -2512,7 +2524,7 @@ def generate_candidate_routes(
2512
  continue
2513
 
2514
  length_m = route_length_m(graph, route_nodes)
2515
- if length_m <= 0:
2516
  continue
2517
  unique_ratio = len(set(route_nodes)) / len(route_nodes)
2518
  if unique_ratio < 0.55:
@@ -2570,7 +2582,7 @@ def generate_candidate_routes(
2570
  selected_label, shape_metrics, is_loop=True
2571
  ),
2572
  }
2573
- if delta <= tolerance:
2574
  accepted.append(row)
2575
  fallback.append(row)
2576
 
@@ -2591,7 +2603,7 @@ def generate_candidate_routes(
2591
  if len(route_nodes) < 10:
2592
  continue
2593
  length_m = route_length_m(graph, route_nodes)
2594
- if length_m <= 0:
2595
  continue
2596
  pts = route_points(graph, route_nodes)
2597
  shape_quality, shape_metrics = route_shape_quality_with_metrics(pts, close_loop=False)
@@ -2639,7 +2651,7 @@ def generate_candidate_routes(
2639
  selected_label, shape_metrics, is_loop=False
2640
  ),
2641
  }
2642
- if delta <= tolerance:
2643
  accepted.append(row)
2644
  fallback.append(row)
2645
 
@@ -2647,7 +2659,7 @@ def generate_candidate_routes(
2647
  key=lambda x: (
2648
  -generation_mode_priority(x),
2649
  -float(x.get("label_generation_prior", 0.0)),
2650
- x["delta"],
2651
  -float(x.get("shape_quality", 0.0)),
2652
  -len(x["nodes"]),
2653
  )
 
2387
  target_distance_m: float,
2388
  tolerance_ratio: float = 0.35,
2389
  selected_label: str | None = None,
2390
+ auto_distance: bool = False,
2391
  ) -> list[dict[str, Any]]:
2392
  nodes = list(graph.nodes)
2393
  if len(nodes) < 8:
 
2416
  shuffled_examples = random.sample(examples, len(examples)) if examples else []
2417
  for attempt in range(guided_attempts):
2418
  try:
2419
+ guided_target_m = (3000.0, 5000.0, 8000.0, 12000.0, 16000.0)[attempt % 5] if auto_distance else target_distance_m
2420
  geo_points = guided_geo_waypoints(
2421
  graph=graph,
2422
  label=str(selected_label),
2423
+ target_distance_m=guided_target_m,
2424
  template=shuffled_examples[attempt % len(shuffled_examples)] if shuffled_examples else None,
2425
  )
2426
  waypoint_nodes = nearest_nodes_for_geo_points(graph, geo_points)
 
2431
  if len(route_nodes) < 18:
2432
  continue
2433
  length_m = route_length_m(graph, route_nodes)
2434
+ if length_m <= 0 or (auto_distance and not 1500.0 <= length_m <= 20000.0):
2435
  continue
2436
  unique_ratio = len(set(route_nodes)) / len(route_nodes)
2437
  if unique_ratio < 0.50:
 
2483
  selected_label, shape_metrics, is_loop=True
2484
  ),
2485
  }
2486
+ if delta <= tolerance or auto_distance:
2487
  accepted.append(row)
2488
  fallback.append(row)
2489
 
2490
+ if auto_distance and selected_label:
2491
+ # Automatic sizing is a drawing search. Random loops cannot satisfy
2492
+ # the requested label, so spend the remaining time ranking guided
2493
+ # road routes instead of generating hundreds of unrelated loops.
2494
+ accepted.sort(key=lambda route: (
2495
+ -float(route.get("shape_quality", 0.0)),
2496
+ -float(route.get("label_generation_prior", 0.0)),
2497
+ ))
2498
+ return accepted[:max(route_count, 24)]
2499
+
2500
  # 2) closed-loop random candidates
2501
  for _ in range(max_attempts):
2502
  try:
 
2524
  continue
2525
 
2526
  length_m = route_length_m(graph, route_nodes)
2527
+ if length_m <= 0 or (auto_distance and not 1500.0 <= length_m <= 20000.0):
2528
  continue
2529
  unique_ratio = len(set(route_nodes)) / len(route_nodes)
2530
  if unique_ratio < 0.55:
 
2582
  selected_label, shape_metrics, is_loop=True
2583
  ),
2584
  }
2585
+ if delta <= tolerance or auto_distance:
2586
  accepted.append(row)
2587
  fallback.append(row)
2588
 
 
2603
  if len(route_nodes) < 10:
2604
  continue
2605
  length_m = route_length_m(graph, route_nodes)
2606
+ if length_m <= 0 or (auto_distance and not 1500.0 <= length_m <= 20000.0):
2607
  continue
2608
  pts = route_points(graph, route_nodes)
2609
  shape_quality, shape_metrics = route_shape_quality_with_metrics(pts, close_loop=False)
 
2651
  selected_label, shape_metrics, is_loop=False
2652
  ),
2653
  }
2654
+ if delta <= tolerance or auto_distance:
2655
  accepted.append(row)
2656
  fallback.append(row)
2657
 
 
2659
  key=lambda x: (
2660
  -generation_mode_priority(x),
2661
  -float(x.get("label_generation_prior", 0.0)),
2662
+ *((x["delta"],) if not auto_distance else ()),
2663
  -float(x.get("shape_quality", 0.0)),
2664
  -len(x["nodes"]),
2665
  )