PHPackages                             survos/place-map-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. survos/place-map-bundle

ActiveSymfony-bundle

survos/place-map-bundle
=======================

Open-source place library (OpenStreetMap point/polygon places) + Symfony UX Map integration: clustered, thumbnail-on-zoom item markers

2.19.1(3w ago)038MITPHPPHP ^8.5

Since Jul 12Pushed 3w agoCompare

[ Source](https://github.com/survos/place-map-bundle)[ Packagist](https://packagist.org/packages/survos/place-map-bundle)[ GitHub Sponsors](https://github.com/kbond)[ RSS](/packages/survos-place-map-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (22)Versions (5)Used By (0)

Place Map Bundle
================

[](#place-map-bundle)

An open-source place library + map view. Two things:

1. **Place library** — a `Place` entity (a precise point, a general area as a GeoJSON polygon, or both), imported from OpenStreetMap/Nominatim with `place:import "Geneva, Switzerland"`.
2. **Map rendering enhancement** — a small Stimulus controller that adds clustered, thumbnail-on-zoom item markers to a [Symfony UX Map](https://symfony.com/bundles/ux-map/current/index.html) (`ux_map()`) map.

This bundle deliberately does **not** ship its own map component or its own Leaflet wrapper. Rendering — tiles, the place polygon, the place marker, `InfoWindow`s — is entirely `symfony/ux-map` + a renderer bridge (e.g. `symfony/ux-leaflet-map`). This bundle only adds the place-library data layer and the one thing `ux-map` doesn't do out of the box: clustered markers whose icon changes (dot → photo thumbnail) as you zoom in.

Requirements
------------

[](#requirements)

- PHP 8.5+, Symfony 8.1+
- Doctrine ORM (ships the `Place` entity)
- The consuming app must itself require `symfony/ux-map` + a renderer bridge. This bundle's JS is written against **`symfony/ux-leaflet-map`** specifically (it grabs the raw Leaflet map/`L` instance off `ux-map`'s own `connect` event) — a different renderer bridge (e.g. Google Maps) will not work with the enhancement controller, though the `Place` library / `PlaceMapBuilder` value-object conversion is renderer-agnostic.
- `survos/imgproxy-bundle` — **optional**. When installed, item thumbnails are signed imgproxy URLs (preset `tiny`); without it, `PlaceMapBuilder::resolveThumb()` passes the source URL through unchanged.
- on par in features/functionality with [https://feature-map-google--fortepan.netlify.app/hu/map/?gc=46.7624,17.2513&amp;gz=15](https://feature-map-google--fortepan.netlify.app/hu/map/?gc=46.7624,17.2513&gz=15)

Why this split (and not a bespoke Leaflet wrapper)
--------------------------------------------------

[](#why-this-split-and-not-a-bespoke-leaflet-wrapper)

`Symfony\UX\Map\Marker` supports a per-marker `extra` payload explicitly documented for "greater flexibility... via a custom Stimulus controller" — but the installed Leaflet bridge (`ux-leaflet-map`) does not round-trip that payload back onto the Leaflet marker instance it creates, so it can't drive an icon swap on zoom. Rather than fork/patch the bridge, item (photo) markers are built directly in `assets/controllers/place_map_enhance_controller.js`, on the same raw Leaflet `map` instance `ux-map` exposes via its `ux:map:connect` event — full control over clustering and icon swapping, zero forked bridge code, and the place-library layer (polygon + place marker, a small fixed set) still goes through `ux-map` normally via `PlaceMapBuilder`.

Commands
--------

[](#commands)

CommandWhat`place:import "" [--code=] [--label=]`Search OpenStreetMap/Nominatim for ``, store its point (and GeoJSON polygon boundary, when OSM has one) as a `Place`. `--code` defaults to a slug of ``. Safe to re-run — updates the existing row.```
php bin/console place:import "Geneva, Switzerland" --code=geneva
```

Configuration
-------------

[](#configuration)

```
survos_place_map:
    # Required by Nominatim's usage policy — identify your app + a contact.
    nominatim_user_agent: '%env(default::PLACE_MAP_USER_AGENT)%'
```

```
# .env
PLACE_MAP_USER_AGENT="YourApp/1.0 (https://your-site.example; contact@your-site.example)"

```

Fetch strategy (cache/retry)
----------------------------

[](#fetch-strategy-cacheretry)

`NominatimClient` fetches through `survos/fetch-bundle`'s `PersistentFetcherInterface`. This matters more here than for most APIs: Nominatim's usage policy caps unauthenticated use at **~1 request/second**, so caching isn't just a performance nicety, it's what keeps repeat `place:import` runs (e.g. re-importing during development) from tripping the rate limit at all.

- **Cache:** keyed by the full request URL, cached **forever** in a SQLite pool at `var/data/fetch_cache.db` — until `forget()`/`force_fetch`. Re-running `place:import` for the same query never re-hits Nominatim.
- **Retry:** up to 5 attempts, full-jitter exponential backoff (200ms base, 10s cap), on transport errors, HTTP 429, and 5xx.

If a place import looks stuck on stale data, check the fetch-bundle cache pool before assuming Nominatim's data changed — `forget()` the specific query URL to force a fresh lookup.

Before 2026-08-08 this used a raw `HttpClientInterface` with no caching at all.

Usage
-----

[](#usage)

Build `ux-map` value objects from the place library with `PlaceMapBuilder`, then render with `ux_map()`. Add the enhancement controller to the **same element** via the `attributes` array so it can merge onto `ux-map`'s own `data-controller` and listen for its `ux:map:connect` event:

```
final class GenevaMapController extends AbstractController
{
    public function __construct(
        private readonly PlaceRepository $places,
        private readonly PlaceMapBuilder $mapBuilder,
    ) {}

    #[Route('/geneva-map')]
    public function __invoke(): Response
    {
        $geneva = $this->places->find('geneva');

        // Item (photo) markers — plain arrays, NOT ux-map Marker objects: the
        // enhance controller builds its own Leaflet markers from these (see README
        // "Why this split"). thumb is resolved through imgproxy when installed.
        $items = array_map(
            fn (Photo $p) => [
                'lat' => $p->lat, 'lng' => $p->lng,
                'title' => $p->title, 'url' => $this->generateUrl('photo_show', ['id' => $p->id]),
                'thumb' => $this->mapBuilder->resolveThumb($p->imageUrl),
            ],
            $this->photoRepository->findGeocoded(),
        );

        return $this->render('geneva_map.html.twig', [
            'polygon' => $this->mapBuilder->polygonFor($geneva),
            'marker' => $this->mapBuilder->markerFor($geneva),
            'items' => $items,
        ]);
    }
}
```

```
{# templates/geneva_map.html.twig #}
{{ ux_map(
    center: polygon ? null : {lat: marker.position.latitude, lng: marker.position.longitude},
    zoom: 12,
    fitBoundsToMarkers: true,
    polygons: polygon ? [polygon] : [],
    markers: marker ? [marker] : [],
    attributes: {
        style: 'height: 480px; width: 100%;',
        'data-controller': survos_stimulus('place-map-bundle', 'place-map-enhance'),
        'data-survos--place-map-bundle--place-map-enhance-items-value': items|json_encode,
        'data-survos--place-map-bundle--place-map-enhance-thumbnail-zoom-value': 13,
    }
) }}
```

`ux_map()`'s own `attributes['data-controller']` handling normalizes whatever string you pass the same way `stimulus_controller()` does — that's why `survos_stimulus('place-map-bundle', 'place-map-enhance')` (which returns the friendly `@survos/place-map-bundle/place-map-enhance` form, never hand-derived) works directly there. The Stimulus **value** attribute names, however, are passed straight through unnormalized, so they must spell out the resolved identifier (`survos--place-map-bundle--place-map-enhance`) explicitly — the same identifier `survos_stimulus()`'s output normalizes to.

Data model
----------

[](#data-model)

`Place` (table `place_map_place`): `code` (PK), `label`, `lat`/`lng` (nullable), `geojson` (nullable — a GeoJSON `Polygon`/`MultiPolygon`), `boundingBox` (nullable `[south, north, west, east]`, cheap for map fit-bounds without parsing `geojson`), `source`/`sourceId` (provenance, e.g. `osm` / `relation/1685488`), `meta` (free-form, e.g. OSM's `display_name`/address parts).

Related bundles
---------------

[](#related-bundles)

This bundle imports and renders individual OSM/Nominatim places one query at a time (`place:import ""`) — a curated point/polygon library, not a name-resolution authority or a bulk matcher. Two companions cover the rest of the "what place is this?" problem:

- [`survos/geonames-bundle`](../geonames-bundle) — country → admin region → city hierarchies from free text, including locale-specific alternate names (e.g. German "Wien" vs. French "Vienne" for the same city). SQLite-backed, no ORM.
- [`survos/poi-bundle`](../poi-bundle) — bulk/automated matching of a literal name against OSM features within a bounding box (e.g. matching a photo caption's place tag to a specific church or school), rather than one deliberately-chosen place at a time.

See [survos/mono#28](https://github.com/survos/mono/issues/28) for how the split was decided.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance95

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

4

Last Release

23d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/21b39551f92ed4143772c622f9e571589c5a72c96ab3c53fe67489ce0d83e806?d=identicon)[tacman1123](/maintainers/tacman1123)

---

Top Contributors

[![tacman](https://avatars.githubusercontent.com/u/619585?v=4)](https://github.com/tacman "tacman (6 commits)")

---

Tags

symfonysymfony-uxmapgeoleafletOpenStreetMap

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/survos-place-map-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/survos-place-map-bundle/health.svg)](https://phpackages.com/packages/survos-place-map-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.7M445](/packages/easycorp-easyadmin-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.4k1.4M241](/packages/sulu-sulu)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
