Skip to content

feat: playground & restroom amenity POI types (#418)#426

Merged
fatherlinux merged 3 commits into
masterfrom
feature/418-amenity-poi-types
May 27, 2026
Merged

feat: playground & restroom amenity POI types (#418)#426
fatherlinux merged 3 commits into
masterfrom
feature/418-amenity-poi-types

Conversation

@fatherlinux
Copy link
Copy Markdown
Member

Summary

Adds Playground and Restroom as map POI types so parents can find parks with good amenities, seeds real locations from OpenStreetMap, and excludes these amenity types from news/events collection.

  • Types (migration 066): playground + restroom icon rows. Name-classified with no activity fallbacks, so a full park that merely lists "Playground" among its activities keeps its own icon. Lowest sort_order so the explicit restroom/playground keyword beats park-name keywords (e.g. mill→historic).
  • icons.default_hidden: amenities appear in the legend but start toggled off to avoid clutter; users opt in.
  • pois.osm_id: provenance + idempotency key (unique partial index).
  • OSM data: backend/data/osm/amenities.json — 240 features inside boundary_type='park' polygons (201 restrooms, 39 playgrounds). Idempotent importer import-osm-amenities.js upserts point POIs by osm_id, named from the OSM tag or "<Park> Restroom/Playground".
  • Collection skip: news_collection_excluded_types (default ["playground","restroom"]); backend/utils/poiClassify.js + the two selection helpers drop excluded types (by name classification or sole-activity match). Verified 0 amenities leak; real parks still collect.
  • Attribution: OSM/ODbL credit added to the satellite base layer (street layer already credits OSM).

No pois-schema change beyond the additive osm_id column; 066 is idempotent.

Closes #418

Deploy steps (beyond image pull + restart)

  1. 066_add_amenity_poi_types.sql runs automatically on container init.
  2. Run the importer once: node /app/migrations/import-osm-amenities.js.

Test plan

  • ./run.sh build (frontend compiled via reload rebuild)
  • Legend shows Playground/Restroom, default off; toggling reveals pins inside parks
  • Amenity POIs have no News/Events tabs; 0 leak into collection; real parks still collect
  • "Valley View Woods Park" keeps its normal icon (regression fixed)
  • Importer idempotent (re-run = 0 inserts); 8/8 poiClassify unit tests; gourmand clean
  • Human verification in browser

🤖 Generated with Claude Code

fatherlinux and others added 3 commits May 27, 2026 04:48
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Playground and Restroom as map POI types so parents can find parks
with good amenities, seed real locations from OpenStreetMap, and exclude
these amenity types from news/events collection.

- migration 066: playground + restroom icon types (name-classified, no
  activity fallbacks so a park that merely offers a playground keeps its
  own icon); pois.osm_id provenance/idempotency column; icons.default_hidden
  so amenities sit in the legend but start toggled off (avoid clutter);
  news_collection_excluded_types setting (default playground,restroom).
- OSM import: backend/data/osm/amenities.json (240 features inside park
  boundaries: 201 restrooms, 39 playgrounds) + idempotent importer
  (import-osm-amenities.js) upserting point POIs by osm_id, named by
  containing park.
- collection skip: backend/utils/poiClassify.js + newsService selection
  drops POIs whose type is excluded (name classification) or whose sole
  activity is a dedicated amenity.
- frontend: default_hidden types start unchecked; OSM (ODbL) attribution
  on the satellite base layer.
- two amenity icons (playground.svg, restroom.svg); poiClassify unit tests.

Closes #418

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fatherlinux
Copy link
Copy Markdown
Member Author

Gatehouse review disposition

Fixed (doc, 1 commit):

  • MED: documented that classifyPoiType's iconConfig must be ordered by sort_order ascending (the implicit priority contract).
  • LOW: commented the xmax = 0 insert/update sentinel in the importer.

Justified — false positives:

  • 2× CRITICAL ("primary_activities is a TEXT[] array → .trim()/.toLowerCase() TypeError"): pois.primary_activities is text, not text[] (confirmed via information_schema: data_type=text; sample value "Hiking, Picnicking, …"). The pg driver returns a plain comma-separated string, so the string methods are correct. The live collection run executed without error and classified correctly (0 amenities leaked, real parks still selected) — which it could not do if the value were an array.

Justified — no change:

  • MED (N+1 upsert in importer): one-time import of 240 rows, run manually at deploy; a bulk statement adds complexity for no meaningful gain at this size.
  • MED (sort_order overloaded for display + classification): pre-existing semantics — the frontend classifier already iterates icons by sort_order; this PR documents it in the migration but doesn't introduce the dual use. Legend display sorts by label separately.
  • MED (iconConfig loaded per filterExcludedTypePois call): called once per collection job (not per POI), so it's one extra cheap query per batch run.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces "Playground" and "Restroom" as first-class map POI types, seeds them using a committed OpenStreetMap snapshot, and excludes these amenity types from news/events collection to save API budget. Feedback was provided to optimize the backend POI classifier by caching compiled regular expressions in matchesWholeWord to prevent performance degradation during batch processing.

Comment on lines +14 to +17
function matchesWholeWord(text, keyword) {
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`\\b${escaped}\\b`, 'i').test(text);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Compiling a new RegExp object on every call to matchesWholeWord inside nested loops for every POI can lead to significant performance degradation during batch processing of hundreds or thousands of POIs. Caching the compiled regular expressions by keyword avoids redundant compilation overhead.

Suggested change
function matchesWholeWord(text, keyword) {
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`\\b${escaped}\\b`, 'i').test(text);
}
const regexCache = new Map();
function matchesWholeWord(text, keyword) {
let regex = regexCache.get(keyword);
if (!regex) {
const escaped = keyword.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&');
regex = new RegExp('\\\\b' + escaped + '\\\\b', 'i');
regexCache.set(keyword, regex);
}
return regex.test(text);
}

@fatherlinux fatherlinux merged commit ccd0e1f into master May 27, 2026
3 checks passed
@fatherlinux fatherlinux deleted the feature/418-amenity-poi-types branch May 27, 2026 17:48
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.

[Feature]: Add Playgrounds and Bathrooms as a POI Type

1 participant