diff --git a/Sources/GeoJSON/BoundingBox.swift b/Sources/GeoJSON/BoundingBox.swift
index 17bbbfb..027ec10 100644
--- a/Sources/GeoJSON/BoundingBox.swift
+++ b/Sources/GeoJSON/BoundingBox.swift
@@ -40,8 +40,17 @@ public enum AnyBoundingBox: BoundingBox, Hashable, Codable {
 	public static var zero: AnyBoundingBox = .twoDimensions(.zero)
 	
 	public func union(_ other: AnyBoundingBox) -> AnyBoundingBox {
-		#warning("Implement `AnyBoundingBox.union`")
-		fatalError("Not implemented yet")
+		switch (self, other) {
+		case let (.twoDimensions(self), .twoDimensions(other)):
+			return .twoDimensions(self.union(other))
+			
+		case let (.twoDimensions(bbox2d), .threeDimensions(bbox3d)),
+			 let (.threeDimensions(bbox3d), .twoDimensions(bbox2d)):
+			return .threeDimensions(bbox3d.union(bbox2d))
+			
+		case let (.threeDimensions(self), .threeDimensions(other)):
+			return .threeDimensions(self.union(other))
+		}
 	}
 	
 	case twoDimensions(BoundingBox2D)
diff --git a/Sources/GeoJSON/GeoJSON+Codable.swift b/Sources/GeoJSON/GeoJSON+Codable.swift
index e388457..70059ec 100644
--- a/Sources/GeoJSON/GeoJSON+Codable.swift
+++ b/Sources/GeoJSON/GeoJSON+Codable.swift
@@ -176,6 +176,39 @@ extension SingleGeometry {
 	
 }
 
+fileprivate enum GeometryCollectionCodingKeys: String, CodingKey {
+	case geoJSONType = "type"
+	case geometries, bbox
+}
+
+extension GeometryCollection {
+	
+	public init(from decoder: Decoder) throws {
+		let container = try decoder.container(keyedBy: GeometryCollectionCodingKeys.self)
+		
+		let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType)
+		guard type == Self.geoJSONType else {
+			throw DecodingError.typeMismatch(Self.self, DecodingError.Context(
+				codingPath: container.codingPath,
+				debugDescription: "Found GeoJSON type '\(type.rawValue)'"
+			))
+		}
+		
+		let geometries = try container.decode([AnyGeometry].self, forKey: .geometries)
+		
+		self.init(geometries: geometries)
+	}
+	
+	public func encode(to encoder: Encoder) throws {
+		var container = encoder.container(keyedBy: GeometryCollectionCodingKeys.self)
+		
+		try container.encode(Self.geoJSONType, forKey: .geoJSONType)
+		try container.encode(self.geometries, forKey: .geometries)
+		try container.encode(self.bbox, forKey: .bbox)
+	}
+	
+}
+
 fileprivate enum AnyGeometryCodingKeys: String, CodingKey {
 	case geoJSONType = "type"
 }
diff --git a/Sources/GeoJSON/Geometries/GeometryCollection.swift b/Sources/GeoJSON/Geometries/GeometryCollection.swift
index 0ffc767..cc1fdc7 100644
--- a/Sources/GeoJSON/Geometries/GeometryCollection.swift
+++ b/Sources/GeoJSON/Geometries/GeometryCollection.swift
@@ -11,10 +11,14 @@ public struct GeometryCollection: CodableGeometry {
 	
 	public static var geometryType: GeoJSON.`Type`.Geometry { .geometryCollection }
 	
-	public var _bbox: AnyBoundingBox { asAnyGeometry.bbox }
+	public var _bbox: AnyBoundingBox { geometries.bbox }
 	
 	public var asAnyGeometry: AnyGeometry { .geometryCollection(self) }
 	
 	public var geometries: [AnyGeometry]
 	
+	public init(geometries: [AnyGeometry]) {
+		self.geometries = geometries
+	}
+	
 }
diff --git a/Sources/GeoJSON/Objects/FeatureCollection.swift b/Sources/GeoJSON/Objects/FeatureCollection.swift
index 46bd03c..bdc26ff 100644
--- a/Sources/GeoJSON/Objects/FeatureCollection.swift
+++ b/Sources/GeoJSON/Objects/FeatureCollection.swift
@@ -17,8 +17,9 @@ public struct FeatureCollection<
 	
 	public var features: [Feature<ID, Geometry, Properties>]
 	
-	// FIXME: Fix bounding box
-	public var bbox: AnyBoundingBox? { nil }
+	public var bbox: AnyBoundingBox? {
+		features.compactMap(\.bbox).reduce(nil, { $0.union($1.asAny) })
+	}
 	
 }
 
diff --git a/Sources/GeoModels/2D/BoundingBox2D.swift b/Sources/GeoModels/2D/BoundingBox2D.swift
index 3661aed..741df10 100644
--- a/Sources/GeoModels/2D/BoundingBox2D.swift
+++ b/Sources/GeoModels/2D/BoundingBox2D.swift
@@ -145,3 +145,9 @@ extension BoundingBox2D: BoundingBox {
 	}
 	
 }
+
+extension BoundingBox2D {
+	
+	public func union(_ bbox3d: BoundingBox3D) -> BoundingBox3D { bbox3d.union(self) }
+	
+}
diff --git a/Sources/GeoModels/3D/BoundingBox3D.swift b/Sources/GeoModels/3D/BoundingBox3D.swift
index 919dde3..4c09a73 100644
--- a/Sources/GeoModels/3D/BoundingBox3D.swift
+++ b/Sources/GeoModels/3D/BoundingBox3D.swift
@@ -110,3 +110,12 @@ extension BoundingBox3D: BoundingBox {
 	}
 	
 }
+
+extension BoundingBox3D {
+	
+	public func union(_ bbox2d: BoundingBox2D) -> BoundingBox3D {
+		let other = BoundingBox3D(bbox2d, lowAltitude: self.lowAltitude, zHeight: .zero)
+		return self.union(other)
+	}
+	
+}
diff --git a/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift b/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift
index 66522eb..725f8fb 100644
--- a/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift
+++ b/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift
@@ -199,4 +199,84 @@ final class GeoJSONEncodableTests: XCTestCase {
 		XCTAssertEqual(string, expected)
 	}
 	
+	func testFeatureCollectionOfGeometryCollectionEncode() throws {
+		struct FeatureProperties: Hashable, Codable {}
+		
+		let featureCollection: FeatureCollection = FeatureCollection(features: [
+			Feature(
+				geometry: GeometryCollection(geometries: [
+					.point2D(Point2D(coordinates: .nantes)),
+					.point2D(Point2D(coordinates: .bordeaux)),
+				]),
+				properties: FeatureProperties()
+			),
+			Feature(
+				geometry: GeometryCollection(geometries: [
+					.point2D(Point2D(coordinates: .paris)),
+					 .point2D(Point2D(coordinates: .marseille)),
+				]),
+				properties: FeatureProperties()
+			),
+		])
+		let data: Data = try JSONEncoder().encode(featureCollection)
+		let string: String = try XCTUnwrap(String(data: data, encoding: .utf8))
+		
+		let expected: String = [
+			"{",
+				"\"type\":\"FeatureCollection\",",
+				// For some reason, `"bbox"` goes here 🤷
+				"\"bbox\":[-1.55366,43.29868,5.36468,48.85719],",
+				"\"features\":[",
+					"{",
+						// For some reason, `"properties"` goes here 🤷
+						"\"properties\":{},",
+						"\"type\":\"Feature\",",
+						"\"geometry\":{",
+							"\"type\":\"GeometryCollection\",",
+							// For some reason, `"bbox"` goes here 🤷
+							"\"bbox\":[-1.55366,44.8378,-0.58143,47.21881],",
+							"\"geometries\":[",
+								"{",
+									"\"type\":\"Point\",",
+									"\"coordinates\":[-1.55366,47.21881],",
+									"\"bbox\":[-1.55366,47.21881,-1.55366,47.21881]",
+								"},",
+								"{",
+									"\"type\":\"Point\",",
+									"\"coordinates\":[-0.58143,44.8378],",
+									"\"bbox\":[-0.58143,44.8378,-0.58143,44.8378]",
+								"}",
+							"]",
+						"},",
+						"\"bbox\":[-1.55366,44.8378,-0.58143,47.21881]",
+					"},",
+					"{",
+						// For some reason, `"properties"` goes here 🤷
+						"\"properties\":{},",
+						"\"type\":\"Feature\",",
+						"\"geometry\":{",
+							"\"type\":\"GeometryCollection\",",
+							// For some reason, `"bbox"` goes here 🤷
+							"\"bbox\":[2.3529,43.29868,5.36468,48.85719],",
+							"\"geometries\":[",
+								"{",
+									"\"type\":\"Point\",",
+									"\"coordinates\":[2.3529,48.85719],",
+									"\"bbox\":[2.3529,48.85719,2.3529,48.85719]",
+								"},",
+								"{",
+									"\"type\":\"Point\",",
+									"\"coordinates\":[5.36468,43.29868],",
+									"\"bbox\":[5.36468,43.29868,5.36468,43.29868]",
+								"}",
+							"]",
+						"},",
+						"\"bbox\":[2.3529,43.29868,5.36468,48.85719]",
+					"}",
+				"]",
+			"}",
+		].joined()
+		XCTAssertEqual(string, expected)
+	}
+	
 }