Procedural Address Resolution — From Placement Identity to World Coordinates

pavelzosim:~/atlas_●SYS.ONLINE / UTC+3

01 Overview

This article continues the procedural environment series:

After generating the environment, routing its placements into Unreal and attaching lighting to those placements, I needed the next layer: turning generated geometry into identifiable places. The current system resolves building, entrance and apartment addresses from the relationships already present in the generated data.

Building 035 / Entrance 001 / Apartment 015 should identify the same place wherever it is used. The numbers should also follow an explicit spatial rule, as real-world addresses do, rather than being assigned independently to each facade.

Generated residential buildings with resolved facade numbers 032 and 039 in Unreal Engine
FIG 01 — Generated buildings with resolved building and entrance numbers. The geometry is procedural; the visible numbers are resolved on the Unreal side.

02 Borrowing structure from real addresses

I have always enjoyed exploring game worlds as much as following their stories and atmosphere. One thing that breaks that sense of exploration for me is an object that clearly suggests interaction but exists only as scenery. A door is the simplest example. Of course, this depends entirely on the type of game: not every environment needs every building to be accessible. But in an exploration-driven space, where locations themselves can carry value, making more of the environment structurally accessible can significantly change how the world feels. For this R&D project, I wanted to push that idea further. Buildings should not only exist as generated geometry; their entrances and apartments should also represent specific, identifiable places.

Suppose the player lives in Building 35, Apartment 15. A decal that says 15 is not enough. The system must be able to determine which entrance contains that apartment, which generated door represents it, and which building all of those elements belong to. The visible labels should be consequences of that structure rather than independent values typed onto meshes.

The trivial solutions are easy:

All three can produce something that looks like an address. None of them creates a reliable address structure.

Real-world numbering gives a useful model. Leeds City Council's policy, for example, assigns odd numbers to the left and even numbers to the right from the start of a street. This gives each side an ordered sequence. Local conventions differ; the diagram illustrates that policy, not a universal rule or a captured street.

The current project resolves buildings from authored anchors, order and a numeric step. Automatically classifying street sides is a possible extension, not implemented behaviour in this slice. Two ordered sides with different starting values and a step of two illustrate how such a numbering policy could work.

FIG 02 — STREET NUMBERING — AN ORDERED SEQUENCE ON EACH SIDE
Odd and even street numberingIllustrative top-down street, numbered upwards from its start. Left-side buildings receive 1, 3 and 5; right-side buildings receive 2, 4 and 6. The project currently supports anchor, order and step; automated side classification is conceptual.005006003004001002ODD · step +2EVEN · step +2START OF STREET
POLICY EXAMPLEEach building receives a number from its side and order along the street. Automatic side classification is a future resolver extension; this is not a screenshot of the current generator.

Randomness is useful for variation, but I do not want it to define the fundamental relationships of the generated environment. Even an invented addressing convention benefits from explicit rules. Once a few constants and relationships exist, the result becomes easier to reason about, query, debug and adapt to different level-design requirements.

The hierarchy below places apartment 015 in entrance 001 of building 035: 035 / 001 / 015.

Floors are an obvious spatial subdivision, but they are not yet a canonical segment in the current implementation. They can be introduced later if level design needs them to participate in address identity rather than remain a derived navigation property.

For example, with three apartments per floor:

Entrance 001
├── Floor 01 → Apartments 001–003
├── Floor 02 → Apartments 004–006
├── Floor 03 → Apartments 007–009
├── Floor 04 → Apartments 010–012
└── Floor 05 → Apartments 013–015

Procedural generation provides identity. The address layer assigns meaning.

FIG 03 — ADDRESS HIERARCHY — FROM BUILDING TO ROOM
Building, entrance, apartment and room relationships Illustrative building 35 has three entrances with 15 apartments each: entrance 1 serves apartments 1 through 15, entrance 2 serves 16 through 30, and entrance 3 serves 31 through 45. Apartment 15 belongs to entrance 1. Dashed lines show a future extension from apartment 15 to rooms 1, 2 and 3, and a local character or script point inside room 3. Room numbers are local to the apartment, not derived from the apartment number. BUILDING 035 ENTRANCE 001Apartments 001–015 ENTRANCE 002Apartments 016–030 ENTRANCE 003Apartments 031–045 APARTMENT 015035 / 001 / 015 ROOM 001local to apartment 015 ROOM 002local to apartment 015 ROOM 003local to apartment 015 Local character / script point
EXAMPLESolid: apartment 015 belongs to entrance 001 of building 035. Dashed: future room extension. Room 003 is numbered within its apartment; its number is not calculated from 015. If global identity is needed, its coordinate becomes 035 / 001 / 015 / Room 003. Character and script points can remain local.

03 One coordinate above the placement pipeline

A number plate displays an address; it does not own it. Other systems use the same canonical coordinate to identify the location.

The canonical value is ST_WorldCoordinate. It carries an ID, a seed and a list of typed address segments. Each ST_AddressSegment contains both:

The distinction becomes especially important in a procedural environment. If a generated layout changes, I want to rebuild derived address state from the current placements rather than preserve stale labels or stale instance indices.

The address book itself is authored. At runtime, the manager copies that data into a working list and resolves generated placements against it. The authored asset remains a source of canonical intent; the runtime nodes are derived state.

This gives me two useful forms of control at the same time:

That coordinate sits above the existing placement pipeline. Houdini supplies identity and relationships; the Unreal address layer interprets them. This lets me add addressing without teaching the generator what a house number means.

The HDA does not decide that wall_island:7 is Building 031.

It publishes a semantic group and placement metadata. The address layer decides that this group represents a building and resolves the number.

Likewise, the scalar-data layer does not know what Display.Number = 31 means. It moves a value to the correct HISM instance. The material does not know what an entrance is. It only turns the value into digits.

FIG 04 — IDENTITY → ADDRESS → CONSUMERS
Identity, address and consumersHoudini / Consumerplacement keys, groups, orderAddress ManagerBuilding / Entrance / ApartmentST_WorldCoordinateone shared logical placeConsumersdisplay / location lookup / runtime systems
MODELOnly the address layer assigns address meaning. Presentation and other consumers reuse the same coordinate.

04 Giving a generated building a number

The generator already publishes wall placements with a semantic group key such as:

wall_island:7

A group represents one ring of walls. The generic generator does not claim that this ring is a "house." That interpretation belongs to the address layer.

For the current project, a wall-island group becomes the unit of a building address.

This is more important than assigning a number to a single plate. A building can have many placements:

The address belongs to the group, not to one mesh.

Conceptually:

wall_island:7
      │
      ▼
ST_BuildingNode
      │
      ├─ group identity
      ├─ stable order
      ├─ optional authored anchor
      └─ resolved building number

Authored anchors. Instead, the designer can bind a known address from the address book to one placement belonging to a building. That placement identifies the semantic group. The group then becomes an anchor for the resolver.

From there, neighbouring buildings can be resolved from their order and a configurable numeric step.

A simplified resolver looks like this:

ResolvedNumber =
    AnchorNumber
    + (BuildingOrder - AnchorOrder) * BuildingNumberStep

The current implementation uses this ordered-anchor pattern. It is not intended to represent every municipal addressing convention; it is one deterministic policy built on top of a more general identity layer.

Why a stable placement key matters. The editor exposes an HISM instance index, but I do not treat that index as persistent identity.

An instance index is valid for the current arrangement of the component. After a procedural rebuild, instances can be reordered. Index 17 may now refer to a different placement.

The project instead carries an FMazePlacementHandle made from:

SourceHismName + PlacementKey

That is the same stability problem I encountered while building the lighting system. Address bindings need to survive the same kinds of rebuilds, so they use the same placement identity instead of treating array position as identity.

Ordering is not the group-key string. A plain text sort produces:

wall_island:1
wall_island:10
wall_island:2

The address layer therefore keeps a separate stable order for the building nodes rather than assuming that the semantic group key itself is a sortable address.

Colored procedural placement points in the Houdini generator
FIG 05 — Two authoring views of the same generated layout: placement points and assembled geometry. Colors are procedural debug data, not resolved address numbers.
Level Address Manager Details showing the address book and authored binding with a source HISM and placement key
FIG 06 — An authored building-address anchor: the address book entry is bound to a source HISM and stable placement key. Unreal Editor Details panel.

05 Counting entrances inside their building

Its number is not resolved globally. It belongs to one building and receives an order inside that parent.

Each ST_EntranceNode stores the source placement, parent building group, procedural source order, resolved local order and final entrance number.

The resolver can be summarized as:

EntranceNumber =
    count(entrances in the same building with lower source order)
    + 1

Source orders 12, 27, 33 and 40 resolve to entrance numbers 001, 002, 003 and 004: gaps in the source values do not create gaps in the labels.

Every building restarts from Entrance 001, because the comparison is scoped to one parent group.

Why I do not use world position. Sorting by world position would force the address resolver to decide which axis or direction represents the intended sequence for every building. A rotated or irregular structure would make that policy increasingly fragile.

The procedural source order already describes the intended order of the entrances. Using it keeps the numbering tied to generated identity rather than to one interpretation of world coordinates.

There is still a boundary condition: equal source-order values are ambiguous. The current "count lower values" rule gives equal ranks to equal values, so duplicates require validation or an explicit tie-break policy.

FIG 08 — SOURCE ORDER → LOCAL ENTRANCE NUMBER
SOURCE ORDER → LOCAL ENTRANCE NUMBERHISM slots: 0 1 2 3Source order: 27 40 12 33Entrance number: 002 004 001 003
MODELEach column is the same placement. Rank is counted within its parent building, independently of the current HISM slot.
Generated entrance door displaying number 003
FIG 09 — Resolved entrance numbers on generated doors: two views along the building facade.

06 The second entrance cannot depend on the first visit

Apartment numbers continue across entrances in the same building.

Suppose two entrances contain 26 apartments each:

Entrance 001 → Apartments 001–026
Entrance 002 → Apartments 027–052

The local order of the first apartment in Entrance 002 is still 0. Its final number depends on something outside the currently loaded entrance:

ApartmentBase =
    sum(ApartmentCount of every preceding Entrance in the same Building)

ApartmentNumber =
    ApartmentBase + LocalApartmentOrder + 1

What if Entrance 002 is visited first?. If each entrance interior is loaded on demand, Entrance 001 may never have been loaded. But Entrance 002 still needs to know that its first apartment is 027.

Loading every interior just to count apartment doors would defeat the purpose of loading those interiors on demand.

Counting visible number labels would also be the wrong dependency. Presentation is not the source of truth for how many apartments exist.

The count belongs to the generated apartment-door geometry.

In the current entrance container:

HISM_Door_Apartment_A

is the source used to obtain the local apartment count.

Stage 1 — Know the count before loading the entrance. Each generated entrance variant has a preset JSON summary. A native helper reads the saved HISM instance count from:

generated_summary.instance_counts.HISM_Door_Apartment_A

The variant catalog associates that result with the entrance variant and level. At startup, the address layer converts the successful results into canonical entrance summaries and applies the counts to the corresponding ST_EntranceNode.

The address manager can therefore establish:

Entrance 001 count = 26
Entrance 002 count = 26

without first visiting either interior.

Stage 2 — Validate against the loaded geometry. When an entrance is actually loaded, its BP_Building reads the live HISM instance count and reports it through BP_LocationRoot.

For a loaded space, the runtime count is treated as authoritative.

If it differs from the preset summary, the system can warn, replace the stored count and refresh the affected numbering.

This lets the preset summary solve the preloading problem without treating cached generation data as permanently authoritative.

Unknown is not zero. This also forced me to distinguish two states that are easy to collapse accidentally:

Known count = 0
Unknown count

A confirmed empty entrance is valid data.

A missing JSON file, an invalid key or an unresolved earlier entrance is not the same thing.

HasApartmentCount exists specifically to preserve that distinction.

If the base number depends on an earlier entrance whose count is unknown, the resolver returns Found = false, clears unresolved numbers and waits for valid data rather than silently treating the missing count as zero.

That distinction is small in code, but important in practice: otherwise a missing count can produce plausible-looking but incorrect addresses.

FIG 10 — ENTRANCE 002 FIRST — WITHOUT PRELOADING 001
ENTRANCE 002 FIRST — WITHOUT PRELOADING 001Preset JSON · Entrance 001 has 26 apartment doorsAddress summary · count known while interior is unloadedEntrance 002 · local apartment order 0 + base 26 + 1Resolved first apartment: 027 · range 027–052
MODELCounts from all earlier entrances must be known. A missing summary remains unknown, not zero.
Preset JSON files and the generated summary of HISM instance counts for the street preset
FIG 11 — Preset files and a generated instance-count summary. This capture shows the street preset; apartment numbering reads the corresponding apartment-door count from an entrance preset.
Unreal viewport showing entrance 001 above an output log of applied entrance numbers and placement keys
FIG 12 — Runtime entrance-number application: a rendered 001 label and log entries linking placement keys to resolved numbers. This capture demonstrates entrance binding, not apartment-count validation.
FIG 13 — Apartment numbering independent of visit order: preset summaries establish the ranges before entrance interiors are loaded. Watch on YouTube.

A variant describes how an interior is generated; a coordinate identifies the place represented by that use of the variant.

Two addresses can use the same interior variant and the same preset data while remaining different logical locations.

For example:

Building 035 / Entrance 001 → Variant A
Building 035 / Entrance 002 → Variant A

This is important for procedural content reuse. Asset identity, generation preset and world identity should not collapse into one field simply because they sometimes point to the same data.

The seed follows the coordinate as part of the runtime address context, while the summary helper only reads the existing generation result. It does not start another generation pass merely to discover a count.

07 Making the resolved number visible

The address manager writes a generic scalar value:

Display.Number

to the resolved placement handle.

MazePlacementExtras maps the named scalar to a custom-data channel on the correct HISM instance:

The material reads the channel and turns the value into digit tiles.

Canonical relationship
        ↓
Resolved address number
        ↓
Display.Number
        ↓
MazePlacementExtras scalar override
        ↓
HISM PerInstanceCustomData
        ↓
Address material
        ↓
"031"

The address layer interprets the number; Extras transports it, and the material renders it.

That is the same separation of responsibility used in the lighting work: one placement identity can drive a completely different type of engine-side data without changing the generator.

The digit material. The current material accepts a number from 0 to 999 and uses a horizontal ten-digit texture atlas.

It supports two input paths:

Both routes feed the same digit-selection logic.

A three-digit display is a presentation limit, not an address-model limit. Supporting larger values would require a wider material implementation, not a different canonical coordinate system.

Maze Placement Extras binding maps Display.Number to custom data index zero on the building label HISM
FIG 14 — Display.Number mapped to custom data index 0 on the building-label HISM. The material reads the same channel. Watch the demonstration on YouTube.
FIG 15 — Address digits rendered from per-instance data. Watch on YouTube.

When a plate shows 000. One useful diagnostic case is a label displaying 000.

That does not prove that the resolved address is zero. In the current material setup it usually means that the expected instance value did not reach the material.

The diagnostic path is therefore explicit:

M_AddressDigits_3 in the Unreal Material Editor: a per-instance custom data input and a custom primitive data input selected by a static switch, feeding a custom AddressDigitUV node that drives a digit atlas texture into emissive color and opacity mask
FIG 16 — M_AddressDigits_3: the number enters on the left, the custom node turns it into atlas UVs, the atlas sample leaves as emissive color and opacity mask. Debug view: yes — Unreal Material Editor, not a rendered result.

08 Where this stands right now

The current slice resolves building numbers from authored anchors, entrance numbers from their order inside a building, and apartment ranges from generated door counts. Preset summaries make those counts available before an interior is visited; loaded geometry checks and corrects them.

The documented first-visit checks cover both entrances in the current 26 + 26 case: visiting 002 first still yields apartments 027–052. The same coordinate now also supports travel into addressed interiors, but the loading and return lifecycle belongs to the next article.

A semantic wall group is not universally a building. The current implementation interprets one wall-island ring as a building. An outer block and an inner courtyard ring may therefore appear as separate groups even if level design wants them to share one address.

That requires a merge rule or a different grouping abstraction. The address layer should not pretend the generator gave it semantic information that it did not.

An authored anchor is still authored. The system derives numbers around anchors. It does not decide which canonical address a level should start from.

Runtime display values are not persistence. The scalar overrides used for display are recomputed at runtime. They are not a save-game system.

Persistent world-state changes would need their own persistence layer.

Source order must be unambiguous. The entrance resolver assumes that source order establishes a rank. Duplicate values need validation or an explicit tie-break rule.

Apartment ranges require valid earlier counts. A later entrance cannot have a trustworthy cumulative base if the count of a preceding entrance is unknown.

The preset-summary system solves that dependency for the currently generated variants, but invalid or missing summaries correctly leave the range unresolved.

Floors are not yet part of the canonical coordinate. They are a likely extension, not a shipped address segment in this slice.

09 What's next

The next spatial subdivision I want to explore is floors. They can begin as a navigation property; whether they become an address segment depends on what level design needs to identify. Rooms can follow the same principle when a character or script needs a specific destination inside an apartment.

I also want to develop a library of several hundred entrance, apartment and room presets. That is a later iteration, not the current catalog size. A preset controls the representation of a location; its coordinate preserves which place that representation belongs to.

The numbering policy can evolve as well: odd/even street sides, district offsets or floor-aware rules are possible extensions. They are not implemented in this slice. Authored anchors and procedural relationships would still provide the control underneath them.

10 Result

Generated groups and placements now resolve to a shared ST_WorldCoordinate. Authored anchors control building numbers, local order controls entrance numbers, and cumulative door counts control apartment ranges—even when earlier entrances have not been visited.

The HISM label displays one segment of that coordinate. Other systems can use the full address without defining the location again.

PROJECT MAP // RELATED WRITE-UPS
  • [01]CURRENTProcedural Address Resolution — From Placement Identity to World Coordinates
  • [02]LIVEControlled Procedurality — How a Maze Generator Became a Level Design System — the generator and its level-design abstractions.
    pavelzosim.com/post/controlled-procedurality
  • [03]LIVEHoudini to Unreal — Typed Placement Routing for HISM and Blueprint Actors — the placement identity and routing pipeline reused by the address layer.
    pavelzosim.com/post/houdini-unreal-typed-placement-routing
  • [04]LIVEThe Light System — Randomized and Synchronized Lighting on Generated Geometry — the Extras scalar-data path and stable placement identity reused for labels.
    pavelzosim.com/post/lighting-generated-geometry
  • [05]DRAFTProcedural Travel Resolution — From World Coordinates to Loaded Interiors — the next article: loading an addressed space and resolving its arrival and return points.
    Coming soon
// END OF ARTICLE // EOF