cityjson-lib 0.6.0

High-level CityJSON 2.0 read/write facade integrating JSON I/O
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# FFI Authoring API Proposal

Status: draft for review only. This document proposes a replacement write-side
API for the shared FFI core and the C++ wrapper. Nothing on this page is
implemented yet.

## Goals

- Make the shared core expressive enough to build the full
  `cityjson_fake_complete.city.json` fixture without raw JSON fragments.
- Replace the current patchwork write API with a small number of consistent,
  typed authoring concepts.
- Keep the low-level FFI ABI generic and binding-friendly.
- Make the C++ wrapper feel like a first-class typed authoring API rather than
  a thin collection of one-off mutators.

## Non-Goals

- Preserve backwards compatibility with the current write-side FFI API.
- Expose every Rust implementation detail 1:1.
- Optimize for handwritten C ergonomics. The core stays C ABI-safe first; the
  C++ wrapper provides the user-facing authoring experience.

## Design Summary

The proposed design has four pillars:

1. The shared core exposes typed model-owned resources and typed draft handles.
2. Recursive CityJSON attribute trees are represented by a typed `Value` API,
   not by raw JSON strings.
3. Geometry authoring is centered on draft objects that match the Rust
   `GeometryDraft` shape: points, linestrings, rings, surfaces, shells, solids,
   and instances.
4. The C++ wrapper provides value types, strong resource IDs, and RAII builder
   objects over the shared core.

## Core FFI Surface

### Core Principles

- Opaque handles own all non-trivial mutable state:
  - `cj_model_t`
  - `cj_value_t`
  - `cj_contact_t`
  - `cj_cityobject_draft_t`
  - `cj_geometry_draft_t`
  - `cj_ring_draft_t`
  - `cj_surface_draft_t`
  - `cj_shell_draft_t`
- Model-owned resources return stable typed IDs:
  - `cj_cityobject_id_t`
  - `cj_geometry_id_t`
  - `cj_geometry_template_id_t`
  - `cj_semantic_id_t`
  - `cj_material_id_t`
  - `cj_texture_id_t`
- Small leaf data stays POD:
  - `cj_vertex_t`
  - `cj_uv_t`
  - `cj_transform_t`
  - `cj_bbox_t`
  - `cj_rgb_t`
  - `cj_rgba_t`
  - `cj_affine_transform_4x4_t`

### New Shared Enums and Plain Structs

```c
typedef uint32_t cj_cityobject_id_t;
typedef uint32_t cj_geometry_id_t;
typedef uint32_t cj_geometry_template_id_t;
typedef uint32_t cj_semantic_id_t;
typedef uint32_t cj_material_id_t;
typedef uint32_t cj_texture_id_t;

typedef enum cj_value_kind_t {
  CJ_VALUE_NULL = 0,
  CJ_VALUE_BOOL = 1,
  CJ_VALUE_INT64 = 2,
  CJ_VALUE_FLOAT64 = 3,
  CJ_VALUE_STRING = 4,
  CJ_VALUE_ARRAY = 5,
  CJ_VALUE_OBJECT = 6,
  CJ_VALUE_GEOMETRY_REF = 7
} cj_value_kind_t;

typedef struct cj_bbox_t {
  double min_x, min_y, min_z;
  double max_x, max_y, max_z;
} cj_bbox_t;

typedef struct cj_rgb_t { float r, g, b; } cj_rgb_t;
typedef struct cj_rgba_t { float r, g, b, a; } cj_rgba_t;

typedef struct cj_affine_transform_4x4_t {
  double elements[16];
} cj_affine_transform_4x4_t;
```

### Value API

The `Value` API is the single generic mechanism for:

- CityObject attributes
- CityObject extra members such as `address` and `children_roles`
- metadata extra members such as `nospec_description`
- root extra members such as `+census`
- contact address objects

It is intentionally typed, recursive, and independent from JSON text.

```c
cj_status_t cj_value_new_null(cj_value_t **out_value);
cj_status_t cj_value_new_bool(bool value, cj_value_t **out_value);
cj_status_t cj_value_new_int64(int64_t value, cj_value_t **out_value);
cj_status_t cj_value_new_float64(double value, cj_value_t **out_value);
cj_status_t cj_value_new_string(cj_string_view_t value, cj_value_t **out_value);
cj_status_t cj_value_new_array(cj_value_t **out_value);
cj_status_t cj_value_new_object(cj_value_t **out_value);
cj_status_t cj_value_new_geometry_ref(cj_geometry_id_t id, cj_value_t **out_value);

cj_status_t cj_value_array_push(cj_value_t *array_value, cj_value_t *element);
cj_status_t cj_value_object_insert(
    cj_value_t *object_value,
    cj_string_view_t key,
    cj_value_t *member_value);

cj_status_t cj_value_free(cj_value_t *value);
```

Notes:

- `cj_value_array_push` and `cj_value_object_insert` transfer ownership of the
  child value into the parent.
- `CJ_VALUE_GEOMETRY_REF` is required because CityJSON attributes can embed
  geometry references, as in the fixture `address.location`.

### Metadata and Root API

The current one-field-at-a-time metadata setters are replaced by grouped,
typed operations.

```c
cj_status_t cj_contact_new(cj_contact_t **out_contact);
cj_status_t cj_contact_set_name(cj_contact_t *contact, cj_string_view_t value);
cj_status_t cj_contact_set_email(cj_contact_t *contact, cj_string_view_t value);
cj_status_t cj_contact_set_role(cj_contact_t *contact, cj_contact_role_t value);
cj_status_t cj_contact_set_website(cj_contact_t *contact, cj_string_view_t value);
cj_status_t cj_contact_set_type(cj_contact_t *contact, cj_contact_type_t value);
cj_status_t cj_contact_set_phone(cj_contact_t *contact, cj_string_view_t value);
cj_status_t cj_contact_set_organization(cj_contact_t *contact, cj_string_view_t value);
cj_status_t cj_contact_set_address(cj_contact_t *contact, cj_value_t *object_value);
cj_status_t cj_contact_free(cj_contact_t *contact);

cj_status_t cj_model_set_metadata_geographical_extent(cj_model_t *model, cj_bbox_t bbox);
cj_status_t cj_model_set_metadata_identifier(cj_model_t *model, cj_string_view_t value);
cj_status_t cj_model_set_metadata_reference_date(cj_model_t *model, cj_string_view_t value);
cj_status_t cj_model_set_metadata_reference_system(cj_model_t *model, cj_string_view_t value);
cj_status_t cj_model_set_metadata_title(cj_model_t *model, cj_string_view_t value);
cj_status_t cj_model_set_metadata_contact(cj_model_t *model, cj_contact_t *contact);
cj_status_t cj_model_set_metadata_extra(
    cj_model_t *model,
    cj_string_view_t key,
    cj_value_t *value);

cj_status_t cj_model_set_root_extra(
    cj_model_t *model,
    cj_string_view_t key,
    cj_value_t *value);

cj_status_t cj_model_add_extension(
    cj_model_t *model,
    cj_string_view_t name,
    cj_string_view_t url,
    cj_string_view_t version);
```

### Appearance Resource API

Appearance resources are model-owned and referenced by typed IDs.

```c
cj_status_t cj_model_add_semantic(
    cj_model_t *model,
    cj_string_view_t semantic_type,
    cj_semantic_id_t *out_id);
cj_status_t cj_model_set_semantic_parent(
    cj_model_t *model,
    cj_semantic_id_t semantic,
    cj_semantic_id_t parent);
cj_status_t cj_model_semantic_set_extra(
    cj_model_t *model,
    cj_semantic_id_t semantic,
    cj_string_view_t key,
    cj_value_t *value);

cj_status_t cj_model_add_material(
    cj_model_t *model,
    cj_string_view_t name,
    cj_material_id_t *out_id);
cj_status_t cj_model_material_set_ambient_intensity(
    cj_model_t *model,
    cj_material_id_t material,
    float value);
cj_status_t cj_model_material_set_diffuse_color(
    cj_model_t *model,
    cj_material_id_t material,
    cj_rgb_t value);
cj_status_t cj_model_material_set_emissive_color(
    cj_model_t *model,
    cj_material_id_t material,
    cj_rgb_t value);
cj_status_t cj_model_material_set_specular_color(
    cj_model_t *model,
    cj_material_id_t material,
    cj_rgb_t value);
cj_status_t cj_model_material_set_shininess(
    cj_model_t *model,
    cj_material_id_t material,
    float value);
cj_status_t cj_model_material_set_transparency(
    cj_model_t *model,
    cj_material_id_t material,
    float value);
cj_status_t cj_model_material_set_is_smooth(
    cj_model_t *model,
    cj_material_id_t material,
    bool value);

cj_status_t cj_model_add_texture(
    cj_model_t *model,
    cj_string_view_t image,
    cj_image_type_t image_type,
    cj_texture_id_t *out_id);
cj_status_t cj_model_texture_set_wrap_mode(
    cj_model_t *model,
    cj_texture_id_t texture,
    cj_wrap_mode_t value);
cj_status_t cj_model_texture_set_texture_type(
    cj_model_t *model,
    cj_texture_id_t texture,
    cj_texture_type_t value);
cj_status_t cj_model_texture_set_border_color(
    cj_model_t *model,
    cj_texture_id_t texture,
    cj_rgba_t value);

cj_status_t cj_model_add_uv_coordinate(
    cj_model_t *model,
    cj_uv_t uv,
    uint32_t *out_uv_index);
cj_status_t cj_model_set_default_material_theme(cj_model_t *model, cj_string_view_t theme);
cj_status_t cj_model_set_default_texture_theme(cj_model_t *model, cj_string_view_t theme);
```

### CityObject Draft API

CityObjects are authored as drafts, inserted into the model, then linked by
typed IDs.

```c
cj_status_t cj_cityobject_draft_new(
    cj_string_view_t id,
    cj_string_view_t cityobject_type,
    cj_cityobject_draft_t **out_draft);
cj_status_t cj_cityobject_draft_set_geographical_extent(
    cj_cityobject_draft_t *draft,
    cj_bbox_t bbox);
cj_status_t cj_cityobject_draft_set_attribute(
    cj_cityobject_draft_t *draft,
    cj_string_view_t key,
    cj_value_t *value);
cj_status_t cj_cityobject_draft_set_extra(
    cj_cityobject_draft_t *draft,
    cj_string_view_t key,
    cj_value_t *value);
cj_status_t cj_model_add_cityobject(
    cj_model_t *model,
    cj_cityobject_draft_t *draft,
    cj_cityobject_id_t *out_id);

cj_status_t cj_model_cityobject_add_geometry(
    cj_model_t *model,
    cj_cityobject_id_t cityobject,
    cj_geometry_id_t geometry);
cj_status_t cj_model_cityobject_add_parent(
    cj_model_t *model,
    cj_cityobject_id_t child,
    cj_cityobject_id_t parent);

cj_status_t cj_cityobject_draft_free(cj_cityobject_draft_t *draft);
```

Notes:

- `cj_model_cityobject_add_parent` updates both parent and child relations.
- Explicit `children_roles` stays an extra member authored through `Value`.

### Geometry Draft API

Geometry authoring follows the Rust draft shape, not the current
boundary-columnar shortcut.

```c
cj_status_t cj_ring_draft_new(cj_ring_draft_t **out_ring);
cj_status_t cj_ring_draft_push_vertex_index(cj_ring_draft_t *ring, uint32_t vertex_index);
cj_status_t cj_ring_draft_push_vertex(cj_ring_draft_t *ring, cj_vertex_t vertex);
cj_status_t cj_ring_draft_add_texture(
    cj_ring_draft_t *ring,
    cj_string_view_t theme,
    cj_texture_id_t texture,
    const uint32_t *uv_indices,
    size_t uv_index_count);

cj_status_t cj_surface_draft_new(
    cj_ring_draft_t *outer,
    cj_surface_draft_t **out_surface);
cj_status_t cj_surface_draft_add_inner_ring(
    cj_surface_draft_t *surface,
    cj_ring_draft_t *inner);
cj_status_t cj_surface_draft_set_semantic(
    cj_surface_draft_t *surface,
    cj_semantic_id_t semantic);
cj_status_t cj_surface_draft_add_material(
    cj_surface_draft_t *surface,
    cj_string_view_t theme,
    cj_material_id_t material);

cj_status_t cj_shell_draft_new(cj_shell_draft_t **out_shell);
cj_status_t cj_shell_draft_add_surface(
    cj_shell_draft_t *shell,
    cj_surface_draft_t *surface);

cj_status_t cj_geometry_draft_new_multi_point(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_multi_line_string(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_multi_surface(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_composite_surface(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_solid(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_multi_solid(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_composite_solid(
    cj_string_view_t lod,
    cj_geometry_draft_t **out_draft);
cj_status_t cj_geometry_draft_new_instance(
    cj_geometry_template_id_t template_id,
    uint32_t reference_vertex_index,
    cj_affine_transform_4x4_t transform,
    cj_geometry_draft_t **out_draft);

cj_status_t cj_geometry_draft_add_point_vertex_index(
    cj_geometry_draft_t *draft,
    uint32_t vertex_index,
    const cj_semantic_id_t *semantic_or_null);
cj_status_t cj_geometry_draft_add_linestring(...);
cj_status_t cj_geometry_draft_add_surface(...);
cj_status_t cj_geometry_draft_add_shell(...);
cj_status_t cj_geometry_draft_add_solid(...);

cj_status_t cj_model_add_geometry(
    cj_model_t *model,
    cj_geometry_draft_t *draft,
    cj_geometry_id_t *out_id);
cj_status_t cj_model_add_geometry_template(
    cj_model_t *model,
    cj_geometry_draft_t *draft,
    cj_geometry_template_id_t *out_id);

cj_status_t cj_geometry_draft_free(cj_geometry_draft_t *draft);
cj_status_t cj_ring_draft_free(cj_ring_draft_t *draft);
cj_status_t cj_surface_draft_free(cj_surface_draft_t *draft);
cj_status_t cj_shell_draft_free(cj_shell_draft_t *draft);
```

Notes:

- Mixed authoring is allowed where it is useful: a ring can accept existing
  vertex indices or new vertices.
- UVs are model-owned, so ring texture assignment references UV indices.
- The old `cj_model_add_geometry_from_boundary` path is removed.

## C++ Wrapper Surface

### Wrapper Goals

- Hide raw C handle management completely.
- Use value types and strong IDs in public signatures.
- Make the common path read like direct model authoring, not ABI plumbing.

### Core Public Types

```cpp
namespace cityjson_lib {

struct BBox { double min_x, min_y, min_z, max_x, max_y, max_z; };
struct Rgb { float r, g, b; };
struct Rgba { float r, g, b, a; };
struct Transform { std::array<double, 3> scale, translate; };
struct AffineTransform4x4 { std::array<double, 16> elements; };

class Value;
class Contact;
class CityObjectDraft;
class RingDraft;
class SurfaceDraft;
class ShellDraft;
class GeometryDraft;

struct CityObjectId { std::uint32_t value; };
struct GeometryId { std::uint32_t value; };
struct GeometryTemplateId { std::uint32_t value; };
struct SemanticId { std::uint32_t value; };
struct MaterialId { std::uint32_t value; };
struct TextureId { std::uint32_t value; };

}  // namespace cityjson_lib
```

### Proposed `Value` API

```cpp
class Value final {
 public:
  static Value null();
  static Value boolean(bool value);
  static Value integer(std::int64_t value);
  static Value number(double value);
  static Value string(std::string value);
  static Value geometry(GeometryId value);
  static Value array();
  static Value object();

  Value& push(Value value);
  Value& insert(std::string key, Value value);
};
```

### Proposed `Model` Authoring API

```cpp
class Model final {
 public:
  static Model create(ModelType type);

  void reserve_import(const ModelCapacities& capacities);
  std::uint32_t add_vertex(const Vertex& vertex);
  std::uint32_t add_template_vertex(const Vertex& vertex);
  std::uint32_t add_uv_coordinate(const UV& uv);

  void set_transform(const Transform& transform);
  void clear_transform();

  void set_metadata_geographical_extent(const BBox& bbox);
  void set_metadata_identifier(std::string_view value);
  void set_metadata_reference_date(std::string_view value);
  void set_metadata_reference_system(std::string_view value);
  void set_metadata_title(std::string_view value);
  void set_metadata_contact(Contact contact);
  void set_metadata_extra(std::string key, Value value);

  void set_root_extra(std::string key, Value value);
  void add_extension(std::string name, std::string url, std::string version);

  SemanticId add_semantic(std::string semantic_type);
  void set_semantic_parent(SemanticId semantic, SemanticId parent);
  void set_semantic_extra(SemanticId semantic, std::string key, Value value);

  MaterialId add_material(std::string name);
  void set_material_ambient_intensity(MaterialId id, float value);
  void set_material_diffuse_color(MaterialId id, Rgb value);
  void set_material_emissive_color(MaterialId id, Rgb value);
  void set_material_specular_color(MaterialId id, Rgb value);
  void set_material_shininess(MaterialId id, float value);
  void set_material_transparency(MaterialId id, float value);
  void set_material_is_smooth(MaterialId id, bool value);

  TextureId add_texture(std::string image, ImageType image_type);
  void set_texture_wrap_mode(TextureId id, WrapMode value);
  void set_texture_type(TextureId id, TextureType value);
  void set_texture_border_color(TextureId id, Rgba value);

  void set_default_material_theme(std::string theme);
  void set_default_texture_theme(std::string theme);

  GeometryId add_geometry(GeometryDraft draft);
  GeometryTemplateId add_geometry_template(GeometryDraft draft);

  CityObjectId add_cityobject(CityObjectDraft draft);
  void add_cityobject_geometry(CityObjectId cityobject, GeometryId geometry);
  void add_cityobject_parent(CityObjectId child, CityObjectId parent);
};
```

### Proposed Draft Types

```cpp
class Contact final {
 public:
  Contact& set_name(std::string value);
  Contact& set_email(std::string value);
  Contact& set_role(ContactRole value);
  Contact& set_website(std::string value);
  Contact& set_type(ContactType value);
  Contact& set_phone(std::string value);
  Contact& set_organization(std::string value);
  Contact& set_address(Value object_value);
};

class CityObjectDraft final {
 public:
  CityObjectDraft(std::string id, std::string type);
  CityObjectDraft& set_geographical_extent(const BBox& bbox);
  CityObjectDraft& set_attribute(std::string key, Value value);
  CityObjectDraft& set_extra(std::string key, Value value);
};

class RingDraft final {
 public:
  RingDraft& push_vertex_index(std::uint32_t index);
  RingDraft& push_vertex(Vertex vertex);
  RingDraft& add_texture(std::string theme, TextureId texture, std::vector<std::uint32_t> uv_indices);
};

class SurfaceDraft final {
 public:
  explicit SurfaceDraft(RingDraft outer);
  SurfaceDraft& add_inner_ring(RingDraft inner);
  SurfaceDraft& set_semantic(SemanticId semantic);
  SurfaceDraft& add_material(std::string theme, MaterialId material);
};

class ShellDraft final {
 public:
  ShellDraft& add_surface(SurfaceDraft surface);
};

class GeometryDraft final {
 public:
  static GeometryDraft multi_point(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft multi_line_string(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft multi_surface(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft composite_surface(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft solid(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft multi_solid(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft composite_solid(std::optional<std::string> lod = std::nullopt);
  static GeometryDraft instance(
      GeometryTemplateId template_id,
      std::uint32_t reference_vertex_index,
      AffineTransform4x4 transform);

  GeometryDraft& add_point(std::uint32_t vertex_index, std::optional<SemanticId> semantic = std::nullopt);
  GeometryDraft& add_linestring(std::vector<std::uint32_t> vertex_indices, std::optional<SemanticId> semantic = std::nullopt);
  GeometryDraft& add_surface(SurfaceDraft surface);
  GeometryDraft& add_shell(ShellDraft shell);
  GeometryDraft& add_solid(ShellDraft outer, std::vector<ShellDraft> inner_shells = {});
};
```

## Example Authoring Style

The target C++ example should be able to read naturally at the model level:

```cpp
auto model = cityjson_lib::Model::create(CJ_MODEL_TYPE_CITY_JSON);

model.set_metadata_title("Complete and fake CityJSON");
model.set_root_extra(
    "+census",
    cityjson_lib::Value::object()
        .insert("percent_men", cityjson_lib::Value::number(49.5))
        .insert("percent_women", cityjson_lib::Value::number(51.5)));

const auto roof = model.add_semantic("RoofSurface");
const auto patio = model.add_semantic("+PatioDoor");
model.set_semantic_parent(patio, roof);
model.set_semantic_extra(roof, "surfaceAttribute", cityjson_lib::Value::boolean(true));

const auto irradiation = model.add_material("irradiation");
const auto winter = model.add_texture(
    "http://www.someurl.org/filename.jpg",
    cityjson_lib::ImageType::Png);

auto solid = cityjson_lib::GeometryDraft::solid("2.1");
// ... add shells, surfaces, semantics, materials, and textured rings ...
const auto building_geometry = model.add_geometry(std::move(solid));

auto building = cityjson_lib::CityObjectDraft("id-1", "BuildingPart");
building.set_attribute("measuredHeight", cityjson_lib::Value::number(22.3));
building.set_extra("address", /* recursive typed value tree */);

const auto building_id = model.add_cityobject(std::move(building));
model.add_cityobject_geometry(building_id, building_geometry);
```

## Explicit Breaking Changes

The current write-side FFI surface is proposed to be replaced, not extended.

Removed or superseded concepts:

- boundary-only geometry insertion as the primary authoring path
- isolated metadata title/identifier setters as the main metadata model
- cityobject mutation by string ID as the main construction mechanism
- ad hoc one-off mutators that do not compose into full model authoring

Retained concepts:

- parse, inspect, cleanup, append, extract, and serialize workflows
- RAII `Model` ownership in C++
- explicit write options and feature-stream helpers

## Review Questions

- Is the value-tree API sufficiently typed, or should geometry references inside
  attribute values be modeled differently?
- Is handle-based cityobject linking acceptable at the core layer, with the C++
  wrapper providing the higher-level ergonomics?
- Should LoD remain string-based at the FFI boundary, or be normalized into a
  dedicated enum/value type before implementation?