Production2025

Points of Interest

Collaborative hotspot mapping through anonymous crowdsourcing

Cover for Points of Interest

Points of Interest started as a proof-of-concept application for mapping nearby activity through anonymous crowd submissions.

The goal was not simply to let users drop markers on a map. The real goal was to transform many small, anonymous location signals into an aggregated live view of local activity.

The project explores how people can report nearby points of interest without creating accounts, while the system turns those reports into heatmaps, activity feeds, contributor statistics, and danger-zone style hotspot indicators.

Points of Interest is designed as a real-time geospatial feedback loop and combines Symfony, React, TypeScript, Leaflet, OpenStreetMap, Mercure, Doctrine, PostgreSQL/PostGIS, anonymous participation, real-time updates, and geospatial interface design.

The Problem

The core problem behind Points of Interest was not displaying a map. The real problem was making local activity visible without forcing users into a heavy reporting workflow.

Most mapping applications are built around fixed places:

Plain text
restaurants

shops

roads

landmarks

addresses

saved locations

But many real-world signals are temporary.

A place can become active for a short period. A zone can become crowded. A location can become relevant because multiple people report it around the same time.

That type of information is not static. It is collective, recent, and local.

Points of Interest explores a simple question:

Can anonymous nearby signals be transformed into a useful real-time map of local activity?

That question creates several technical challenges:

Plain text
How do users submit a signal quickly?

How do we avoid requiring authentication?

How do we prevent spam or abusive submissions?

How do we validate that a signal is actually near the user?

How do we aggregate raw points into useful hotspot data?

How do we update every connected client in real time?

How do we keep the interface understandable on top of a dense map?

A basic marker map would not be enough.

The system needed to collect signals, validate them, store them, aggregate them, stream updates, and render them as a clear geospatial interface.

That turned Points of Interest from a simple map demo into a full-stack real-time mapping system.

Product Direction

Points of Interest started with a simple idea:

Let people anonymously report nearby activity and visualize the result as a live heatmap.

That idea quickly became more interesting because the product needed to balance participation, privacy, and signal quality.

A useful version of the system needed to:

  • detect or request the user’s current location;

  • allow the user to submit a nearby point from the map;

  • validate that the submitted point is close enough to the user;

  • identify contributors without accounts;

  • rate-limit submissions per anonymous client;

  • store raw signals with timestamps;

  • aggregate recent signals into density cells;

  • expose snapshots through an API;

  • broadcast live updates to connected clients;

  • display hotspots, recent activity, and contributor statistics;

  • keep the interface usable without requiring authentication.

The product direction was intentionally lightweight.

The user should not need to create an account, fill a long form, or understand a complex reporting model. They should open the app, allow location access, tap the map, confirm the signal, and immediately see the shared map update.

System Architecture

Points of Interest is organized around five main layers:

Mermaid

Each layer has a clear responsibility.

The client layer handles interaction and visualization. The API layer receives and exposes signal data. The validation layer protects the quality of submissions. The persistence layer stores signals. The real-time layer keeps all clients synchronized.

The system follows this flow:

Plain text
User action

  -> map click

  -> confirmation dialog

  -> API submission

  -> anonymous client identification

  -> rate limit check

  -> proximity validation

  -> signal persistence

  -> snapshot rebuild

  -> Mercure broadcast

  -> client heatmap update

That structure keeps the project understandable while still covering the important parts of a real-time collaborative application.

Backend API Layer

The backend is a Symfony API responsible for receiving signals, returning snapshots, and coordinating live updates.

The API exposes one central resource:

Plain text
/api/signals

It supports two main operations:

Plain text
GET  /api/signals  -> return a recent signal snapshot

POST /api/signals  -> submit a new signal

The controller is intentionally thin. It delegates business logic to dedicated services:

Mermaid

This is the right design choice.

The controller should not know how to hash a user, validate geospatial distance, aggregate density cells, or publish real-time updates. It should only translate HTTP requests into application actions.

Snapshot Endpoint

The GET /api/signals endpoint returns a snapshot of recent activity.

The snapshot includes:

Plain text
clientKey

points

density

latestByUser

totals

updatedAt

This gives the frontend everything it needs to render the map, heat layer, contributor count, recent activity, and user-specific state.

The snapshot model is useful because the frontend does not need to reconstruct the whole domain from raw database rows. It receives a prepared view of the current activity state.

Submission Endpoint

The POST /api/signals endpoint accepts a signal payload containing:

Plain text
signalLocation

userLocation

That distinction matters.

The user’s current location is used to validate proximity. The signal location is the point they want to report. Those two coordinates may be the same, but they are not always the same.

For example, a user may be standing near a place and submit a signal slightly ahead of them on the map.

The backend checks whether that submitted point is within the allowed range before storing it.

Domain Model

At the center of the backend is the Signal entity.

A signal represents one anonymous report submitted by a user.

Mermaid

The entity stores:

Plain text
id

userKey

userLocation

signalLocation

createdAt

This model is small, but it captures the essential information.

The system does not need a user account, profile, password, session table, or social identity. It only needs an anonymous contributor key, the submitted point, the user’s approximate position at submission time, and the timestamp.

That makes the data model lightweight and focused.

Anonymous Participation Layer

One of the most important product decisions was to avoid authentication.

Users can participate without creating an account.

Instead of storing identity, the backend derives an anonymous client key from the request IP address.

Mermaid

This gives the application enough identity to support:

Plain text
rate limiting

latest signal by contributor

contributor statistics

user-specific latest signal

without forcing users to register.

It is a pragmatic privacy tradeoff for a proof of concept.

The system does not claim to provide perfect anonymity. IP-derived identifiers can still carry privacy implications, especially if logs or raw addresses are retained. But for this prototype, the design keeps the user experience frictionless while avoiding direct account-based identity.

Validation Layer

Crowdsourced systems become useless if submissions are too easy to abuse.

Points of Interest handles this with two backend protections:

Plain text
rate limiting

proximity validation

Rate Limiting

Each anonymous client key is passed through Symfony’s rate limiter before a signal is accepted.

The goal is simple:

One person should not be able to flood the map with fake activity.

The rate limiter protects the system from repeated submissions while keeping the interaction model simple for normal users.

Proximity Validation

The backend also checks that the submitted signal is close enough to the user’s actual location.

That matters because the map should represent nearby reality, not arbitrary remote reporting.

The validation flow looks like this:

Mermaid

The distance is calculated using a haversine-style distance function.

That is exactly the kind of validation this product needs. It is simple, explainable, and good enough for checking whether a reported point is reasonably close to the contributor.

Snapshot and Aggregation Layer

Raw signals are useful for storage, but they are not enough for visualization.

The frontend needs a higher-level representation:

Plain text
Where are the points?

Where is activity dense?

Who submitted recently?

How many contributors are active?

When was the map last updated?

The snapshot builder turns recent signals into a compact response.

Mermaid

The density aggregation groups signals by rounded coordinates and increments an intensity counter per bucket.

That produces heatmap-ready cells:

Plain text
lat

lng

intensity

This is a smart design for the prototype because the frontend can render density without doing expensive grouping work itself.

The backend owns aggregation. The frontend owns visualization.

Real-Time Layer

Points of Interest uses Mercure to broadcast live signal updates.

When a signal is stored, the backend dispatches a message. A message handler rebuilds the recent snapshot and publishes it to a Mercure topic.

Connected clients subscribe through Server-Sent Events.

Mermaid

This gives the application its collaborative feel.

A user submits a signal, and other connected users can see the heatmap update without refreshing the page.

That is the difference between a static map and a live crowd map.

Frontend Map Layer

The frontend is a React and Vite application built around a Leaflet map.

Its job is to turn the backend snapshot into an interactive geospatial interface.

The main UI pieces are:

Plain text
map viewport

heatmap layer

user location marker

confirmation dialog

header overlay

sidebar panels

recent activity feed

hotspot list

status and error feedback

The frontend flow looks like this:

Mermaid

The frontend keeps the complex parts separated into hooks.

That is a strong architectural choice because map rendering, live data, derived statistics, and UI state are all different concerns.

Leaflet Heatmap

The map layer uses Leaflet and leaflet.heat to render density cells as a heatmap.

The heatmap receives backend-generated density cells and normalizes intensity values before rendering them.

The client also supports map tile providers, including OpenStreetMap and Mapbox-style tiles.

Mermaid

The result is more useful than showing individual markers only.

Markers show events. Heatmaps show patterns.

That distinction is the heart of the project.

Interaction Design

The user interaction is intentionally minimal:

Plain text
open the app

allow location access

view nearby activity

click on the map

confirm the signal

watch the heatmap update

The application does not ask for a name, email, password, profile, or category.

That is the right call for this prototype.

The core experiment is about frictionless participation. Every extra field would make the system heavier and distract from the main idea.

Confirmation Before Submission

A map click does not immediately submit a signal.

It opens a confirmation dialog first.

That small interaction matters because map clicks can be accidental. Confirming the point gives the user a chance to verify the location before contributing to the shared heatmap.

Local Radius Filtering

The frontend focuses on nearby activity.

It filters visible density cells, points, and latest-user signals around the user’s current location.

That keeps the experience local instead of turning the map into a global dump of unrelated points.

For this prototype, locality is the product.

Data Flow

The complete application data flow looks like this:

Mermaid

This is the main engineering value of the project.

It turns individual reports into a shared, live spatial view.

API Contract

The API contract is intentionally compact.

A snapshot response contains:

TypeScript
interface Snapshot {

  clientKey?: string;

  points: Signal[];

  density: SignalDensityCell[];

  latestByUser: Signal[];

  totals: {

    points: number;

    contributors: number;

  };

  updatedAt: string;

}

A signal contains:

TypeScript
interface Signal {

  id: string;

  signalLocation: Point;

  createdAt: string;

  userKey: string;

}

A density cell contains:

TypeScript
interface SignalDensityCell {

  lat: number;

  lng: number;

  intensity: number;

}

This shape is clean because it matches the UI directly.

The frontend does not need to understand database rows. It receives exactly the data needed for:

Plain text
map points

heatmap cells

contributor count

recent activity

latest signal by user

last update time

Infrastructure Layer

The local infrastructure uses Docker Compose for the supporting services.

The backend is designed around:

Plain text
Symfony 7

PHP 8.4

Doctrine ORM

PostgreSQL / PostGIS

Mercure

Adminer

The frontend is designed around:

Plain text
React

TypeScript

Vite

Leaflet

leaflet.heat

Zustand

i18next

Sonner

Tailwind-style UI components

The infrastructure can be summarized like this:

Mermaid

This stack fits the project well.

Symfony handles backend structure, validation, messaging, and persistence. React handles interactive UI. Leaflet handles the geospatial interface. Mercure handles real-time streaming. PostgreSQL/PostGIS gives the backend a geospatial-ready storage foundation.

Quality and CI

The repository includes quality checks for both backend and frontend.

The backend quality workflow runs:

Plain text
PHPStan

ECS coding standards

PHPUnit

The frontend quality workflow runs:

Plain text
ESLint

Prettier

TypeScript type checking

That matters because this is not just a throwaway map demo.

The project has enough structure to be maintained, tested, and extended.

Mermaid

The important decision is separating backend and frontend checks.

Server changes trigger PHP workflows. Client changes trigger Node workflows. That keeps CI focused and cheaper to run.

Key Engineering Decisions

Build the product around live snapshots

The project does not stream individual low-level database events to the frontend.

Instead, it streams updated snapshots.

That is a good choice for this prototype because the UI wants the current state of the map, not a complex event log.

A snapshot gives the frontend a simple truth:

Plain text
Here are the current points.

Here is the current density.

Here are the current contributors.

Here is the latest update time.

Keep users anonymous

The application avoids authentication completely.

That lowers friction and makes the prototype easier to use.

The anonymous client key still gives the system enough structure for rate limiting and contributor statistics, but without introducing account management.

That is the right tradeoff for a crowdsourced local signal experiment.

Validate proximity on the backend

The frontend can guide the user, but the backend must enforce the rule.

Points of Interest checks the distance between the user location and the submitted signal location on the server.

That prevents the client from becoming the source of truth.

Never trust the browser for this kind of validation. That would be dumb.

Separate submission from visualization

Submission and visualization are separate flows.

A user submits one signal. The system rebuilds the snapshot. Every client receives the updated view.

That keeps the mental model simple:

Plain text
write signal

rebuild state

broadcast state

render state

Use heatmaps instead of only markers

Individual markers are useful, but they do not communicate density well.

Heatmaps reveal patterns.

For a crowdsourced hotspot system, density is the product. The heatmap is not decoration. It is the main interface.

Use dedicated frontend hooks

The React code separates concerns through hooks:

Plain text
useUserLocation

useHotspotFeed

useLeafletHeatmap

useFeedDerivations

useAppStore

That keeps the app easier to reason about.

Map lifecycle, Mercure subscriptions, derived hotspot lists, geolocation, and UI state should not all live in one giant component.

Use Mercure for real-time updates

Mercure is a strong fit for this use case because the frontend only needs server-to-client updates.

The client does not need a full bidirectional WebSocket protocol. Server-Sent Events are enough.

That makes the real-time layer simpler.

Keep the API contract UI-friendly

The API returns density cells, latest signals by user, totals, and updated timestamps directly.

That keeps frontend rendering straightforward.

The backend prepares the data. The frontend presents it.

What I Learned

The biggest lesson from Points of Interest is that real-time mapping is not mainly about maps.

It is about trust, latency, aggregation, and interaction design.

A map is only the surface. Underneath it, the system needs to decide which signals are valid, how often a user can submit, how points become density, how updates reach clients, and how the interface avoids overwhelming the user.

I also learned that anonymity changes the architecture.

Without accounts, the system still needs a way to distinguish contributors, enforce limits, and show “my latest signal.” The anonymous client key solves enough of that problem for a prototype while keeping participation lightweight.

The project also showed that raw points are not the final product.

A list of coordinates is just data. A heatmap is interpretation. Density cells, contributor totals, recent activity, and danger-zone panels turn raw submissions into something a user can understand quickly.

Finally, the project reinforced a simple full-stack principle:

The backend should protect the data. The frontend should make the data feel alive.

Points of Interest does both.

Conclusion

Points of Interest is a full-stack proof of concept for collaborative hotspot mapping through anonymous crowdsourcing.

Its main value is not that users can click on a map. The value is the full loop around that action: anonymous identification, proximity validation, rate limiting, signal persistence, snapshot aggregation, real-time broadcasting, and heatmap visualization.

The system turns small individual reports into a shared live view of local activity.

That is the core idea behind Points of Interest:

Make nearby crowd activity visible without accounts, friction, or heavy reporting workflows.

More projects

View all

2025

Basango

Towards a scalable and intelligent system for Congolese news curation

Read

2025

PHP Packages Graph

Exploring the php ecosystem through packages published on packagist.org

Read