From 0dbd6375fcbcaef4d4a9afa438aa8c6e99b4f1c6 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 12:50:15 -0700 Subject: [PATCH] fix: center named places on their own location A place's bounds can reach far from the place itself; Tokyo's include remote Pacific islands, so the middle of its box is open ocean. Keep the geocoder's point for a named place and use it as the area's center, while bounds still decide what lies inside the area. --- src/tools/area.js | 12 ++++++++++-- src/tools/area.test.mjs | 19 ++++++++++++++++++- src/tools/places.js | 12 ++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/tools/area.js b/src/tools/area.js index ff61077bc..e494c6782 100644 --- a/src/tools/area.js +++ b/src/tools/area.js @@ -75,7 +75,11 @@ export async function resolveArea(area, { services, signal } = {}) { 'invalid_arguments', `No place matched "${area.place}"; try a more specific name or a bbox`, ); - return { label: place.name || area.place, ...place.bounds }; + return { + label: place.name || area.place, + ...place.bounds, + ...(place.point ? { point: place.point } : {}), + }; } function circleArea(lat, lon, radiusKm) { @@ -151,9 +155,13 @@ export function distanceKm(a, b) { return 2 * EARTH_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(h))); } -/** The center of a resolved area, handling boxes across the antimeridian. */ +/** + * The center of a resolved area: a radius area's center, a named place's own + * point, or the middle of a box (handling boxes across the antimeridian). + */ export function areaCenter(area) { if (area.center) return { lat: area.center.lat, lon: area.center.lon }; + if (area.point) return { lat: area.point.lat, lon: area.point.lon }; const width = area.west <= area.east ? area.east - area.west diff --git a/src/tools/area.test.mjs b/src/tools/area.test.mjs index fac64690a..99df85684 100644 --- a/src/tools/area.test.mjs +++ b/src/tools/area.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { areaContains, distanceKm, resolveArea } from './area.js'; +import { areaCenter, areaContains, distanceKm, resolveArea } from './area.js'; import { placeFromGeocodeResult } from './places.js'; test('an area needs exactly one form', async () => { @@ -91,6 +91,7 @@ test('geocode results become bounded places', () => { { name: 'Oslo, Norway', bounds: { west: 10.5, south: 59.8, east: 10.9, north: 60 }, + point: { lat: 59.9, lon: 10.7 }, }, ); assert.deepEqual( @@ -100,3 +101,19 @@ test('geocode results become bounded places', () => { ); assert.equal(placeFromGeocodeResult({ geometry: {} }), null); }); + +test('a named place centers on its own point, not the middle of its bounds', async () => { + // Tokyo's prefecture bounds reach remote Pacific islands. + const places = { + resolve: async () => ({ + name: 'Tokyo, Japan', + bounds: { west: 136.07, south: 20.21, east: 153.99, north: 35.9 }, + point: { lat: 35.6769, lon: 139.7639 }, + }), + }; + const tokyo = await resolveArea({ place: 'Tokyo' }, { services: { places } }); + assert.deepEqual(areaCenter(tokyo), { lat: 35.6769, lon: 139.7639 }); + assert.equal(areaContains(tokyo, { lat: 27.09, lon: 142.19 }), true); + const box = await resolveArea({ bbox: [0, 0, 10, 10] }); + assert.deepEqual(areaCenter(box), { lat: 5, lon: 5 }); +}); diff --git a/src/tools/places.js b/src/tools/places.js index 64d9da201..e7f4f8084 100644 --- a/src/tools/places.js +++ b/src/tools/places.js @@ -13,7 +13,11 @@ import { ToolError } from './catalog.js'; const POINT_RADIUS_DEGREES = 0.25; -/** Resolve place names to `{ name, bounds: { west, south, east, north } }` or null. */ +/** + * Resolve place names to `{ name, bounds: { west, south, east, north }, point }` + * or null. `point` is the place's own location, which can lie far from the + * middle of its bounds (a region with remote islands, for example). + */ export function createGeocodePlaceService({ fetchImpl = (...args) => globalThis.fetch(...args), } = {}) { @@ -54,7 +58,11 @@ export function placeFromGeocodeResult(result) { east: Math.min(180, location.lng + POINT_RADIUS_DEGREES), north: Math.min(90, location.lat + POINT_RADIUS_DEGREES), }; - return { name: result.formatted_address || null, bounds }; + return { + name: result.formatted_address || null, + bounds, + point: { lat: location.lat, lon: location.lng }, + }; } /** Search points of interest; `configured` is false when the server has no search key. */