Skip to content

Rewrite the backend in TypeScript with PostGIS location search - #1

Merged
savikthk merged 3 commits into
mainfrom
feat/ts-backend-location
Oct 3, 2026
Merged

savikthk merged 3 commits into
mainfrom
feat/ts-backend-location

Conversation

@savikthk

@savikthk savikthk commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Summary

Stage 1 of the plan: search by location that actually works, on a TypeScript backend instead of C++.

The old search found the place only through free-text Nominatim geocoding ("метро Тверская" landed on the Okhotny Ryad bus stop, so the 800 m circle moved almost a full radius), never used the user's own location, silently searched the whole city when geocoding failed and ranked venues inside the radius without looking at distance.

Now the algorithm does the work and GigaChat has a narrow role:

  • Rules first: a vocabulary of categories, cuisines, moods, company, features and budgets plus an offline gazetteer from OpenStreetMap (240 metro stations, 667 landmarks, 132 district polygons), matched by Russian word stems.
  • Search in PostGIS: 800 m around a station or landmark, the real district polygon, or 1 km around the user (Near me in the Mini App). When nothing fits, the radius widens to 1.5, 2.5 and 4 km before the category is relaxed, and notes in the response say what happened.
  • Ranking by wishes, taste and distance, one branch per chain.
  • GigaChat only interprets a query the rules did not understand and picks among the top 10 candidates the algorithm found (ids validated). After a network failure it is paused for two minutes and the app answers by the algorithm alone; Nominatim stays as a cached fallback with the same pause.

Changes

  • backend/: Node 24, Fastify 5, zod, PostgreSQL 16 + PostGIS; migrations, rule parser and gazetteer, ranking, search and taste services, GigaChat and Nominatim adapters, places:fetch and db:load scripts. The C++ backend and load_to_db.py are removed.
  • db/Dockerfile: PostgreSQL 16 with PostGIS that runs natively on Apple Silicon; docker-compose.yml builds it on the same data volume.
  • frontend/: Near me button (Telegram LocationManager, browser geolocation outside Telegram), a line that explains where the search went, distances on cards, venue data rendered with textContent.
  • CI: lint, typecheck and tests of the backend with a PostGIS service container instead of the C++ build.
  • README: themed banner, badges, real query examples, a Mermaid diagram of the search.

Testing

  • 84 unit and integration tests, the latter on a real PostGIS, npm run lint and npm run typecheck pass.
  • Live on the full dataset: «бар у метро Тверская» gives bars 62–130 m from the station, «ресторан у Большого театра» restaurants from 109 m without the network, «кафе на Арбате» cafés inside the district, «боулинг у Тверской» widens to 2.5 km with a note, «кофейня рядом» with a location finds coffee 34 m away. GigaChat answered live once the network was back (ranked_by: "llm").

After merging, run docker compose up -d --build and cd backend && npm ci && npm run db:load once: the database image now includes PostGIS and the schema is migrated on start.

The C++ backend searched by place only through free-text Nominatim
geocoding ("метро Тверская" landed on the Okhotny Ryad bus stop), ignored
the user's own location, silently dropped the location when geocoding
failed and ranked venues inside the radius without looking at distance.

The new backend (Node 24, Fastify, zod, PostgreSQL + PostGIS) works
algorithm first:
- a rule parser with a vocabulary of categories, cuisines, moods, company,
  features and budgets and a gazetteer of 240 metro stations and 132
  district polygons from OpenStreetMap, matched by word stems;
- search with ST_DWithin around a station, a landmark or the user, or
  inside a district polygon, widening 800 m to 4 km before relaxing the
  category, with notes that say what happened;
- ranking by wishes, taste and distance, one branch per chain.

GigaChat is narrow: it interprets a query only when the rules found
nothing or a place stayed unresolved, its answer is checked against
closed lists, it may only reorder the top 10 candidates, and an
unreachable service is paused for two minutes. Nominatim stays as a
cached fallback with the same pause.

The Mini App gets a Near me button (Telegram LocationManager, browser
geolocation outside Telegram), a line that explains where it searches,
distances on cards, and renders venue data with textContent.
Nominatim is slow and was unreachable for long stretches, so a query
like "ресторан у Большого театра" depended on a network call. The
gazetteer now also holds 667 notable landmarks from OpenStreetMap
(theatres, museums, parks, gardens, stadiums, squares with a Wikidata
link inside the MKAD) with their alternative names, matched by word
stems like stations: "у Большого театра", "в парке Горького",
"у Патриарших прудов" resolve without the network. A station still wins
over a landmark of the same name. Nominatim remains the fallback.
The README opens like the app: a banner in its own design language
(dark ground with lime blobs, Sekuya title, a mock of the search pill,
the context line and a swipe card for a real bar 60 m from Tverskaya),
light and dark variants picked by the GitHub theme, text outlined so no
fonts or scripts load, 64 KB each with glyphs reused through <use>.
Below it: call to action and stack badges, a navigation row, a table of
real queries and what the app answers, a Mermaid diagram of the search
and collapsible sections for Telegram setup, the API, data, structure
and tests.
@savikthk
savikthk merged commit 2b0bdc0 into main Oct 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant