GeoJSON for indoor mapping: where it breaks, and how to fix it
GeoJSON was designed for streets, parcels and countries. Point it at a building and it runs out of words: it has no floors, no rooms inside buildings and no local coordinates. None of that makes it the wrong format. It just means you need a few conventions, and here they are.
GeoJSON (RFC 7946) describes a geometry and a free-form properties object. That simplicity is why every map library reads it. It is also why indoor projects drift: each team invents its own way to say "this room is on floor 2 of building B", and no two files agree. Below are the six problems you hit first, with the fix most teams settle on.
1. Coordinates must be longitude and latitude
Floor plans come in millimetres or metres from a local origin. RFC 7946 is strict: coordinates are WGS 84 longitude and latitude, and the old crs member was removed from the spec. A plan in metres will either be rejected or land somewhere off the coast of West Africa, at 0°, 0°.
Fix: georeference the plan before you export it, which means placing, scaling and rotating it onto the real building. Keep 7 decimal places (about 1 cm). The spec itself notes that 6 decimals are already about 10 cm, which is enough for rooms but tight for desks.
2. There is no concept of floors
A position can have a third value, but RFC 7946 defines it as altitude or elevation, in metres. That isn't a floor number. Two floors of the same building have identical footprints, so in 2D they sit exactly on top of each other.
Fix: give every feature a level property and filter on it when you render.
- Use an integer ordinal for logic:
"level": 0for the ground floor,1for the floor above,-1for the basement. - Use a separate display name for people:
"level_name": "Mezzanine". - This mirrors existing schemes. OpenStreetMap tags indoor features with
level=*, and Apple's IMDF (an OGC Community Standard) uses anordinalon its level features.
3. A FeatureCollection is flat
Buildings are trees: site → building → floor → room → desk. GeoJSON has one flat list of features. Nesting a FeatureCollection inside another isn't valid GeoJSON, and most libraries won't read it.
Fix: keep the list flat and express the tree with properties. Two common patterns:
- IDs and parent references: give each feature a stable top-level
id, and point children at their parent with"level_id"or"room_id". This is how IMDF links units to levels. - A path or group name:
"group": "Level 1"or"group_path": ["Building A", "Level 1"]. It's simpler to read and filter, but harder to rename.
4. Overlapping shapes have no meaning
A desk sits inside a room, and a room sits inside a floor outline. GeoJSON happily stores overlapping polygons, but nothing in the file says which one is the room and which is furniture. Click handlers and area calculations then pick the wrong one.
Fix: add a category, and render in a fixed order.
"category": "room" | "corridor" | "desk" | "stair" | "door". OpenStreetMap usesindoor=room,indoor=corridorandindoor=areafor the same idea.- Draw floor outlines first, rooms next, furniture and points last, and give small shapes priority on click.
5. Doors, walls and openings
Navigation needs to know where you can walk through. Walls are usually implied by the gaps between room polygons, and doors are often missing entirely.
Fix: model doors explicitly, either as Point features with "category": "door" and a "connects": ["room-12", "corridor-1"] list, or as short LineString features along the wall. IMDF calls these openings.
6. Holes, winding and styling
- Holes: an atrium is a polygon with an inner ring. RFC 7946 asks for counter-clockwise outer rings and clockwise holes. Many tools ignore this, and most renderers cope, but validators will flag it.
- Styling isn't part of GeoJSON. The informal simplestyle-spec keys (
stroke,stroke-width,fill,fill-opacity,marker-color) are the closest thing to a convention. geojson.io and GitHub read them.
The fixes at a glance
| Problem | Plain GeoJSON | Common convention | In a FloorJSON export |
|---|---|---|---|
| Local coordinates | Must be lon/lat | Georeference before export | Placed on the map, exported as lon/lat |
| Floors | Not defined | level + level_name | group per floor; add level as a custom attribute |
| Hierarchy | Flat list only | IDs and parent references | group |
| What a shape is | Not defined | category | shape_type (geometry kind) + custom attributes |
| Styling | Not defined | simplestyle-spec | stroke, fill, fill-opacity, marker-color |
| Labels | Not defined | A name or label key | name, plus label, label_size, label_color |
| Holes | Inner rings | RFC 7946 winding order | Outer rings CCW; holes not supported yet |
Which approach should you use?
- Publishing to Apple Maps, or exchanging with venue platforms: use IMDF. It is GeoJSON-based but strict, with defined feature types (venue, building, level, unit, opening).
- Contributing to OpenStreetMap: follow Simple Indoor Tagging.
- Your own app, dashboard or website: plain GeoJSON with a small, consistent set of properties is the pragmatic choice. Start from the properties schema examples rather than inventing keys project by project.
FAQ
Can GeoJSON store multiple floors of a building?
Yes, as separate features with a level property such as "level": 1. GeoJSON has no built-in floor concept, so filter by that property when you render.Should I use the third coordinate for floors?
No. RFC 7946 defines the third value as altitude or elevation in metres. Use it for real heights if you need them, and a level property for floor numbers.Can I nest a FeatureCollection inside another?
Not in valid GeoJSON. Keep one flat FeatureCollection and express the hierarchy with ids and parent references, or with a group property.Is IMDF better than plain GeoJSON?
For Apple Maps and venue platforms, yes: it is a defined standard. For your own maps it is often more structure than you need, and plain GeoJSON with consistent properties is easier to start with.Skip the hand-rolled properties
FloorJSON exports floor plans with a consistent set of properties (names, groups, colours and labels) so you start from a structure instead of inventing one.
Open FloorJSON