feat(places): descriptions from the place's own website

Overture has no descriptions, and the encyclopaedias only describe the
places that have an article, which is a fraction of a percent. What
Overture does have is the address of the page the description is on, for
43 percent of places, and those pages publish a summary in JSON-LD or
og:description precisely so machines can read it.

The API fetches each page once and keeps the summary; every instance
reads it from there. TREK never opens a business's website itself, which
would mean one request per instance per place against small servers.

In the ladder it sits below the encyclopaedias and above Google: an
article about this exact place beats the operator's own copy, but for a
restaurant or a shop the operator's summary is the only one that exists.
Credited by host and linked back, with no licence claimed.
This commit is contained in:
Maurice
2026-09-06 00:24:12 +02:00
committed by Maurice
parent b3d69aa760
commit 715993feaf
7 changed files with 217 additions and 14 deletions
@@ -35,6 +35,13 @@ vi.mock('../../../src/utils/ssrfGuard', () => ({
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
const { mockTrekPlacesById } = vi.hoisted(() => ({
mockTrekPlacesById: vi.fn(async (_gers: string): Promise<unknown> => null),
}));
vi.mock('../../../src/nest/maps/trek-places.client', () => ({
trekPlacesById: mockTrekPlacesById,
}));
import { db } from '../../../src/db/database';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { PlaceEnrichmentService } from '../../../src/nest/place-enrichment/place-enrichment.service';
@@ -81,6 +88,8 @@ beforeEach(() => {
mockDbGet.mockReset();
mockDbGet.mockReturnValue(undefined);
mockDbRun.mockReset();
mockTrekPlacesById.mockReset();
mockTrekPlacesById.mockResolvedValue(null);
});
describe('identity resolution', () => {
@@ -339,3 +348,70 @@ describe('filling Google gaps from the free sources', () => {
});
})
describe("the description on the place's own website", () => {
// The ordinary case, and the one nothing covered before: a restaurant has no
// encyclopaedia article and never will, but it does publish a summary of
// itself in JSON-LD for machines to read. 43 percent of Overture places carry
// a website; a fraction of a percent carry a wiki tag.
const GERS_REQ = {
lat: 54.0879,
lng: 12.1408,
name: "L'Osteria",
placeId: 'gers:abc-123',
lang: 'de',
};
it('ENRICH-090: quotes the site and credits it by URL', async () => {
mockTrekPlacesById.mockResolvedValue({
description: { text: 'Pizza in Rostock, seit 2015.', sourceUrl: 'https://losteria.net/rostock' },
});
const out = await make(mapsStub()).enrich(1, GERS_REQ);
expect(mockTrekPlacesById).toHaveBeenCalledWith('abc-123');
expect(out.description).toEqual({
text: 'Pizza in Rostock, seit 2015.',
source: 'website',
sourceUrl: 'https://losteria.net/rostock',
// No licence claim: the operator published a summary for machines, they
// did not grant terms. Naming and linking the source is what is owed.
license: null,
});
});
it('ENRICH-091: leaves the encyclopaedias ahead of it', async () => {
// An article about this exact place, written by someone with no stake in
// it, beats the operator's own copy. The website only fills the gap the
// encyclopaedias leave, which is almost every business.
mockTrekPlacesById.mockResolvedValue({ description: { text: 'Unser Restaurant.', sourceUrl: 'https://x.de' } });
const maps = mapsStub({
details: vi.fn(async () => ({ place: { source: 'openstreetmap', wikipedia: 'de:Irgendwas' } })),
fetchWikiExtract: vi.fn(async () => EXTRACT),
});
const out = await make(maps).enrich(1, GERS_REQ);
expect(out.description?.source).toBe('wikipedia');
});
it('ENRICH-092: asks only for places that came from the API', async () => {
await make(mapsStub()).enrich(1, { ...GERS_REQ, placeId: 'ChIJsomethinggoogle' });
expect(mockTrekPlacesById).not.toHaveBeenCalled();
});
it('ENRICH-093: a failing API costs the place nothing else', async () => {
mockTrekPlacesById.mockRejectedValue(new Error('places api down'));
const maps = mapsStub({
details: vi.fn(async () => ({ place: { source: 'openstreetmap', name: "L'Osteria" } })),
});
await expect(make(maps).enrich(1, GERS_REQ)).resolves.toMatchObject({ description: null });
});
it('ENRICH-094: ignores an empty or whitespace-only summary', async () => {
mockTrekPlacesById.mockResolvedValue({ description: { text: ' ', sourceUrl: 'https://x.de' } });
const out = await make(mapsStub()).enrich(1, GERS_REQ);
expect(out.description).toBeNull();
});
});