Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/quiet-ambient-places.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@openmapx/core": minor
---

Add regional ambient-place policy, generation metadata and canonical tile identity contracts.

Normalize actual indexed OSM category formats and rank corroborated cultural landmarks conservatively in ambient policy version 2.

Keep source-fresh retained GERS snapshot identities resolvable after publication replacement.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ OpenMapX is a complete mapping platform you run on your own infrastructure: sear

- **Complete mapping platform** — geocoding, traffic-aware and EV routing, public-transit planning and navigation, live vehicles, street-level imagery, POI search, knowledge enrichment, reviews, crowd reports, weather, and dozens of overlays
- **Two-layer plugin system** — _services_ (containers: Valhalla, Nominatim, MOTIS, …) and _integrations_ (app features: providers, overlays, data sources, tools). Both support community plugins from any Git URL
- **105 built-in integrations** and **29 built-in services** rendered into a generated `docker-compose.yml`
- **106 built-in integrations** and **29 built-in services** rendered into a generated `docker-compose.yml`
- **Self-host the provider stack** — routing, geocoding, transit, tiles, search, and data pipelines have local/open implementations; the lightweight app, database, cache, proxy, and data-manager form the required core
- **Open data** — OpenStreetMap, GTFS via Transitous, Wikidata, Wikipedia, Mapillary, NASA, NOAA, ECCC, DWD, MeteoAlarm, OpenAQ, USGS, NPS, and more
- **Privacy-first** — no third-party analytics; most upstream calls are proxied through your API server, and unavoidable direct browser asset loads are explicitly documented
Expand Down Expand Up @@ -64,7 +64,7 @@ graph TD
Traefik --> Martin["martin :3002<br/>PostGIS vector tiles"]

subgraph "API Server"
IntHost["Integration Host<br/>105 built-in + community"]
IntHost["Integration Host<br/>106 built-in + community"]
Orchestrators["Domain orchestrators<br/>(geocoding, routing, transit, ...)"]
SvcRegistry["Service Registry"]
Bindings["Capability Bindings"]
Expand Down
171 changes: 171 additions & 0 deletions apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"name": "admin",
"description": "Operator-facing administration of integrations, services, settings and jobs."
},
{
"name": "ambient-places",
"description": "Routes under /api/ambient-places."
},
{
"name": "attribution",
"description": "Routes under /api/attribution."
Expand Down Expand Up @@ -144,6 +148,111 @@
}
],
"paths": {
"/api/admin/ambient-places/build": {
"post": {
"operationId": "postApiAdminAmbientPlacesBuild",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"name",
"bounds"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"bounds": {
"type": "array",
"minItems": 4,
"maxItems": 4,
"items": {
"type": "number"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"admin"
],
"x-openmapx-auth": "admin"
}
},
"/api/admin/ambient-places/enabled": {
"post": {
"operationId": "postApiAdminAmbientPlacesEnabled",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"admin"
],
"x-openmapx-auth": "admin"
}
},
"/api/admin/ambient-places/rollback": {
"post": {
"operationId": "postApiAdminAmbientPlacesRollback",
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"admin"
],
"x-openmapx-auth": "admin"
}
},
"/api/admin/ambient-places/status": {
"get": {
"operationId": "getApiAdminAmbientPlacesStatus",
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"admin"
],
"x-openmapx-auth": "admin"
}
},
"/api/admin/audit": {
"get": {
"operationId": "getApiAdminAudit",
Expand Down Expand Up @@ -1917,6 +2026,68 @@
"x-openmapx-auth": "admin"
}
},
"/api/ambient-places/manifest": {
"get": {
"operationId": "getApiAmbientPlacesManifest",
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"ambient-places"
],
"x-openmapx-auth": "public"
}
},
"/api/ambient-places/tiles/{generation}/{z}/{x}/{y}.mvt": {
"get": {
"operationId": "getApiAmbientPlacesTilesGenerationZXYMvt",
"parameters": [
{
"schema": {
"type": "string"
},
"in": "path",
"name": "generation",
"required": true
},
{
"schema": {
"type": "string"
},
"in": "path",
"name": "z",
"required": true
},
{
"schema": {
"type": "string"
},
"in": "path",
"name": "x",
"required": true
},
{
"schema": {
"type": "string"
},
"in": "path",
"name": "y",
"required": true
}
],
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"ambient-places"
],
"x-openmapx-auth": "public"
}
},
"/api/attribution": {
"get": {
"operationId": "getApiAttribution",
Expand Down
55 changes: 55 additions & 0 deletions apps/api/src/routes/__tests__/places.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ vi.mock("../../utils/cache.js", () => ({
TTL: { places: { detail: 86400 }, photos: 3600 },
}));

const mockAmbientPlace = vi.fn().mockResolvedValue(null);
vi.mock("@openmapx/core/ambient-places-server", () => ({
readAmbientPlaceByGers: (...args: unknown[]) => mockAmbientPlace(...args),
}));

// App setup

let app: FastifyInstance;
Expand Down Expand Up @@ -1504,3 +1509,53 @@ describe("pickMoreSpecificWebsite", () => {
).toBe("https://restaurant-mueller.de/");
});
});

describe("published ambient GERS details without the search provider", () => {
it("resolves the published canonical identity without nearest-place fallback", async () => {
mockAmbientPlace.mockResolvedValueOnce({
generation: "published-generation",
place: {
id: "osm:node/9007199254740993",
gersId: "gers-a",
name: "Klinik",
names: { en: "Clinic" },
coordinates: [6.08, 50.77],
category: "hospital",
rank: 380,
minZoom: 13,
tenant: false,
sources: "osm,overture",
},
});
const response = await app.inject({
method: "GET",
url: "/places/overture:gers-a?lat=50.77&lng=6.08&name=Clinic&lang=en",
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
id: "osm:node/9007199254740993",
name: "Clinic",
ids: { osm: "node/9007199254740993", overture: "gers-a" },
});
expect(mockLookupByNameAndCoords).not.toHaveBeenCalled();
expect(mockLookupByCoords).not.toHaveBeenCalled();
expect(mockHashKey).toHaveBeenCalledWith(
"cache:place",
expect.objectContaining({ ambientGeneration: "published-generation" }),
);
});
it("preserves existing fallback for an unpublished disabled-provider deep link", async () => {
mockAmbientPlace.mockResolvedValueOnce(null);
mockIsEnabledIntegrationScheme.mockReturnValue(false);
mockLookupByNameAndCoords.mockResolvedValueOnce(MOCK_PLACE);
mockGetPlaceKnowledge.mockResolvedValue({ externalIds: {} });
mockBuildReviewLinks.mockReturnValue([]);
const response = await app.inject({
method: "GET",
url: "/places/overture:not-published?lat=50.77&lng=6.08&name=Clinic",
});
expect(response.statusCode).toBe(200);
expect(mockAmbientPlace).toHaveBeenCalled();
expect(mockLookupByNameAndCoords).toHaveBeenCalled();
});
});
Loading
Loading