This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new d45c243d83 When reading a geometry from a database, adjust the number 
of CRS dimensions depending on whether Z or M coordinates are present.
d45c243d83 is described below

commit d45c243d83cd72fd737d73a08b3b8aee028eda9b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Nov 2 18:38:43 2026 +0100

    When reading a geometry from a database, adjust the number of CRS 
dimensions depending on whether Z or M coordinates are present.
---
 .../apache/sis/geometry/wrapper/Dimensions.java    |  91 +++++++++
 .../apache/sis/geometry/wrapper/GeometryType.java  |   6 +-
 .../sis/geometry/wrapper/GeometryWrapper.java      |  16 +-
 .../apache/sis/geometry/wrapper/esri/Wrapper.java  |  11 ++
 .../sis/geometry/wrapper/jts/GeometryWalker.java   |  35 +++-
 .../org/apache/sis/geometry/wrapper/jts/JTS.java   |  11 +-
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |  24 +--
 .../sis/geometry/wrapper/DimensionsTest.java       |  95 +++++++++
 .../main/org/apache/sis/referencing/CRS.java       | 220 +++++++++++----------
 .../main/org/apache/sis/referencing/CommonCRS.java |   2 +-
 .../referencing/EllipsoidalHeightSeparator.java    |  69 ++++---
 .../apache/sis/referencing/IdentifiedObjects.java  |   3 +-
 .../sis/referencing/StandardDefinitions.java       |  20 +-
 .../main/org/apache/sis/referencing/cs/Codes.java  |  55 ++++--
 .../sis/referencing/cs/CoordinateSystems.java      |  92 +++++----
 .../apache/sis/referencing/cs/package-info.java    |   2 +-
 .../internal/shared/EllipsoidalHeightCombiner.java | 114 ++++++++---
 .../shared/ReferencingFactoryContainer.java        |   6 +-
 .../internal/shared/ReferencingUtilities.java      |  15 +-
 .../operation/CoordinateOperationFinder.java       |   2 +-
 .../operation/CoordinateOperationRegistry.java     |   2 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |  32 ++-
 .../org/apache/sis/referencing/cs/CodesTest.java   |  35 +++-
 .../shared/EllipsoidalHeightCombinerTest.java      |   6 +-
 .../sis/storage/sql/feature/GeometryGetter.java    |  41 +++-
 .../org/apache/sis/geometries/adapter/Wrapper.java |  19 +-
 26 files changed, 704 insertions(+), 320 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Dimensions.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Dimensions.java
index 1141216ddb..efa7857dc7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Dimensions.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Dimensions.java
@@ -16,11 +16,24 @@
  */
 package org.apache.sis.geometry.wrapper;
 
+import java.util.logging.Filter;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.cs.AxisDirection;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.referencing.internal.shared.AxisDirections;
+import org.apache.sis.referencing.internal.shared.EllipsoidalHeightCombiner;
+import org.apache.sis.referencing.internal.shared.ReferencingFactoryContainer;
+import org.apache.sis.util.collection.BackingStoreException;
 
 
 /**
  * Identification of which dimensions are contained in a sequence of 
coordinate tuples.
+ * Can also {@linkplain #adjust(CoordinateReferenceSystem, SingleCRS) adjust a 
<abbr>CRS</abbr>}
+ * to the layout described by an enumeration value.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -97,4 +110,82 @@ public enum Dimensions {
             default: throw new 
IllegalArgumentException(Resources.format(Resources.Keys.UnsupportedGeometryObject_1,
 count));
         }
     }
+
+    /**
+     * Returns an enumeration value for the given coordinate reference system.
+     * This method does not try to provide an accurate description of the 
given <abbr>CRS</abbr>.
+     * It only returns an estimated best match.
+     *
+     * @param  crs  the coordinate reference system for which to get an 
enumeration value, or {@code null}.
+     * @return enumeration value for this given <abbr>CRS</abbr>. Never null 
but may be inaccurate.
+     */
+    public static Dimensions forCRS(final CoordinateReferenceSystem crs) {
+        boolean hasZ = 
AxisDirections.indexOfColinear(crs.getCoordinateSystem(), AxisDirection.UP) >= 
0;
+        int count = CRS.getDimensionOrZero(crs);
+        if (hasZ) count--;  // Number of dimensions other than vertical.
+        return forZorM(hasZ, count > Geometries.BIDIMENSIONAL);
+    }
+
+    /**
+     * Derives a <abbr>CRS</abbr> with the dimensions specified by this 
enumeration value.
+     * If the given {@code base} <abbr>CRS</abbr> contains the (<var>X</var>, 
<var>Y</var>),
+     * <var>Z</var> and/or <var>M</var> dimensions identified by this 
enumeration value,
+     * these dimensions will be selected and put in the order implied by this 
enumeration.
+     * If some dimensions need to be added, the following heuristics are 
applied:
+     *
+     * <ul>
+     *   <li>If the given {@code base} has no {@linkplain 
CRS#getHorizontalComponent horizontal component},
+     *     the {@linkplain CommonCRS#defaultGeographic() default geographic 
<abbr>CRS</abbr>} is used.</li>
+     *   <li>If the given {@code base} has no {@linkplain 
CRS#getVerticalComponent vertical component} and
+     *     a <var>Z</var> component is requested, the default is ellipsoidal 
height in metres.</li>
+     *   <li>If the given {@code base} has no {@linkplain 
CRS#getTemporalComponent temporal component} and
+     *     a <var>M</var> component is requested, the default is the given 
{@code defaultM} argument.</li>
+     * </ul>
+     *
+     * @param  base           <abbr>CRS</abbr> containing the dimensions to 
use in the <abbr>CRS</abbr> to return.
+     * @param  factories      factories to use for creating new 
<abbr>CRS</abbr>, or {@code null} for the default.
+     * @param  defaultM       default <abbr>CRS</abbr> for the <abbr>M</abbr> 
dimension, or {@code null} if none.
+     * @param  warningFilter  whether to log warnings, or {@code null} for the 
default behavior.
+     * @return a <abbr>CRS</abbr> with the dimensions identified by this 
enumeration value.
+     * @throws FactoryException if an error occurred while querying the 
authority factory or creating a <abbr>CRS</abbr>.
+     */
+    @SuppressWarnings("element-type-mismatch")
+    public CoordinateReferenceSystem adjust(final CoordinateReferenceSystem 
base,
+                                            final ReferencingFactoryContainer 
factories,
+                                            final SingleCRS defaultM,
+                                            final Filter warningFilter)
+            throws FactoryException
+    {
+        CoordinateReferenceSystem crs;
+        try {
+            boolean isAuthoritative = false;
+            crs = hasZ ? CRS.getGeospatialComponent(base)
+                       : CRS.getHorizontalComponent(base);
+            if (crs == null) {
+                crs = CommonCRS.defaultGeographic();
+                isAuthoritative = true;
+            }
+            if (hasZ && CRS.getDimensionOrZero(crs) <= 
Geometries.BIDIMENSIONAL) {
+                SingleCRS toAdd = CRS.getVerticalComponent(base, true);
+                if (toAdd == null) {
+                    toAdd = CommonCRS.Vertical.ELLIPSOIDAL.crs();
+                }
+                crs = new 
EllipsoidalHeightCombiner(factories).createCompoundCRS(crs, toAdd);
+                isAuthoritative = false;
+            }
+            // If the CRS has been created by this method, replace by an 
authoritative definition.
+            if (!(isAuthoritative || crs == base || 
CRS.getSingleComponents(base).contains(crs))) {
+                crs = CRS.fromAuthority(crs, (factories == null) ? null : 
factories.getCRSAuthorityFactory(), warningFilter);
+            }
+            if (hasM) {
+                SingleCRS toAdd = CRS.getTemporalComponent(base);
+                if (toAdd != null || (toAdd = defaultM) != null) {
+                    crs = new 
EllipsoidalHeightCombiner(factories).createCompoundCRS(crs, toAdd);
+                }
+            }
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(FactoryException.class);
+        }
+        return crs;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryType.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryType.java
index df50a9b927..25c0e22ade 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryType.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryType.java
@@ -28,7 +28,7 @@ import org.apache.sis.util.internal.shared.Constants;
 
 /**
  * Implementation-neutral description of the type of geometry.
- * The name of each enumeration value is the name in WKT format.
+ * The name of each enumeration value is the name in <abbr>WKT</abbr> format.
  * The ordinal value is the binary code, ignoring thousands.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -297,6 +297,8 @@ public enum GeometryType {
     /**
      * Returns the type of geometry collection if this type is for a component.
      * If this enumeration value is not a component type, returns {@code null}.
+     *
+     * @return geometry collection type, or {@code null} if this type is not a 
component.
      */
     public final GeometryType collection() {
         return isCollection ? null : related;
@@ -307,6 +309,8 @@ public enum GeometryType {
      * Example of collection types are {@link #MULTIPOINT}, {@link 
#MULTILINESTRING},
      * {@link #MULTIPOLYGON} or {@link #GEOMETRYCOLLECTION}.
      * If this enumeration value is not a collection type, returns {@code 
null}.
+     *
+     * @return collection component type, or {@code null} if this type is not 
a collection.
      */
     public final GeometryType component() {
         return isCollection ? related : null;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
index 68a09fd6ff..3da5724255 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
@@ -82,7 +82,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * the value in geometry instances is sometime 0 or null. In such case, 
this {@code crs}
      * field may come from other sources such as characteristics of the {@code 
FeatureType}.
      *
-     * @see #getCoordinateDimension()
+     * @see #getCoordinateDimensions()
      * @see #getCoordinateReferenceSystem()
      * @see #setCoordinateReferenceSystem(CoordinateReferenceSystem)
      */
@@ -162,6 +162,16 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
         this.crs = crs;
     }
 
+    /**
+     * Returns an identification of which dimensions are for the coordinates 
of this geometry.
+     * The default value is {@link Dimensions#XY}.
+     *
+     * @return the coordinate dimensions (never {@code null}).
+     */
+    public Dimensions getCoordinateDimensions() {
+        return Dimensions.XY;
+    }
+
     /**
      * Returns the dimension of the coordinates that define this geometry.
      * It must be the same as the dimension of the coordinate reference system 
for this geometry.
@@ -169,8 +179,8 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @return the coordinate dimension.
      */
     @Override
-    public int getCoordinateDimension() {
-        return Geometries.BIDIMENSIONAL;
+    public final int getCoordinateDimension() {
+        return getCoordinateDimensions().count;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
index 4b97c98ea0..d15899e9a9 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
@@ -38,6 +38,7 @@ import com.esri.core.geometry.SpatialReference;
 import org.opengis.geometry.DirectPosition;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.wrapper.Dimensions;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.filter.sqlmm.SQLMM;
@@ -83,6 +84,16 @@ final class Wrapper extends GeometryWrapper {
         return geometry;
     }
 
+    /**
+     * Returns an identification of which dimensions are for the coordinates 
of this geometry.
+     *
+     * @return the coordinate dimensions (never {@code null}).
+     */
+    @Override
+    public Dimensions getCoordinateDimensions() {
+        return Dimensions.forZorM(geometry.hasZ(), geometry.hasM());
+    }
+
     /**
      * Returns the <abbr>ESRI</abbr> envelope as an Apache <abbr>SIS</abbr> 
implementation.
      *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
index 96033f0ad8..1b4be2c145 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
@@ -23,22 +23,23 @@ import org.locationtech.jts.geom.LineString;
 import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.wrapper.Dimensions;
 import org.apache.sis.util.resources.Errors;
 
 
 /**
  * Walks through the component of a geometry for finding the number of 
dimensions
- * and the range of <abbr>z</abbr> and <abbr>m</abbr> values.
+ * and the range of <var>z</var> and <var>M</var> values.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
 final class GeometryWalker {
     /**
-     * Whether to search for ranges of <var>z</var> and <var>M</var> values.
-     * If {@code false}, this method will check only for the number of 
dimensions.
+     * Whether to iterate over the coordinate sequences for the ranges of 
<var>z</var> and <var>M</var> values.
+     * If {@code false}, this method will check only for the number of 
dimensions and their types.
      */
-    private boolean wantZM;
+    private boolean wantRangeOfZM;
 
     /**
      * Whether the coordinate sequence declares to have <var>z</var> or 
<var>M</var> values.
@@ -65,7 +66,7 @@ final class GeometryWalker {
     /**
      * Creates a new walker.
      * By default, the walker does <em>not</em> search for ranges of 
<var>z</var> and <var>M</var> values.
-     * If this search is desired, the {@link #wantZM()} method must be invoked 
after construction.
+     * If this search is desired, the {@link #wantRangeOfZM()} method must be 
invoked after construction.
      */
     GeometryWalker() {
     }
@@ -73,8 +74,8 @@ final class GeometryWalker {
     /**
      * Requests the search of ranges of <var>z</var> and <var>M</var> values.
      */
-    final void wantZM() {
-        wantZM = true;
+    final void wantRangeOfZM() {
+        wantRangeOfZM = true;
         minZ = minM = Double.POSITIVE_INFINITY;
         maxZ = maxM = Double.NEGATIVE_INFINITY;
     }
@@ -88,13 +89,23 @@ final class GeometryWalker {
         return dimension;
     }
 
+    /**
+     * Returns an identification of which dimensions are for the coordinate 
values.
+     *
+     * @return the coordinate dimensions (never {@code null}).
+     * @throws IllegalArgumentException if the number of dimensions if less 
than 2 or more than 3.
+     */
+    final Dimensions dimensions() {
+        return Dimensions.forCount(dimension(), hasM);
+    }
+
     /**
      * Sets the <var>z</var> and <var>M</var> coordinates in the given 
envelope.
      *
      * @param  env  the envelope where to set the coordinates.
      */
     final void setZM(final GeneralEnvelope env) {
-        if (wantZM) {
+        if (wantRangeOfZM) {
             final int limit = env.getDimension();
             int target = Factory.BIDIMENSIONAL;
             if (target < limit) {
@@ -130,7 +141,7 @@ final class GeometryWalker {
         } else if (geometry instanceof Polygon) {
             final var polygon = (Polygon) geometry;
             scan(polygon.getExteriorRing());
-            if (wantZM) {
+            if (wantRangeOfZM) {
                 final int n = polygon.getNumInteriorRing();
                 for (int i=0; i<n; i++) {
                     scan(polygon.getInteriorRingN(i));
@@ -161,7 +172,7 @@ final class GeometryWalker {
      */
     private void addDimensionAndRanges(final CoordinateSequence cs) {
         dimension = Math.max(dimension, cs.getDimension());
-        if (wantZM) {
+        if (wantRangeOfZM) {
             final int n = cs.size();
             if (cs.hasZ()) {
                 hasZ = true;
@@ -179,6 +190,10 @@ final class GeometryWalker {
                     if (m > maxM) maxM = m;
                 }
             }
+        } else {
+            // Some implementations may iterate over coordinates. So invoke 
only if necessary.
+            if (!hasZ) hasZ = cs.hasZ();
+            if (!hasM) hasM = cs.hasM();
         }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
index 770fa2d0cf..6121e1e8a1 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
@@ -73,10 +73,12 @@ public final class JTS {
      * <ul>
      *   <li>Geometry {@linkplain Geometry#getUserData() user data} is an 
instance of {@code CoordinateReferenceSystem}.</li>
      *   <li>{@linkplain Geometry#getUserData() user data} is a (@link Map} 
with a value for the {@value #CRS_KEY} key.</li>
-     *   <li>Geometry SRID is strictly positive, in which case it is 
interpreted as an EPSG code.</li>
+     *   <li>Geometry <abbr>SRID</abbr> is strictly positive, in which case it 
is interpreted as an <abbr>EPSG</abbr> code.
+     *       The <abbr>CRS</abbr> built that way is often two-dimensional.</li>
      * </ul>
      *
      * If none of the above is valid, {@code null} is returned.
+     * The current implementation does not verify the number of dimensions.
      *
      * @param  source  the geometry from which to get the CRS, or {@code null}.
      * @return the coordinate reference system, or {@code null} if none.
@@ -114,11 +116,12 @@ public final class JTS {
 
     /**
      * Sets the Coordinate Reference System (CRS) in the specified geometry. 
This method overwrites any previous
-     * user data; it should be invoked only when the geometry is known to not 
store any other information.
+     * user data, do it should be invoked only when the geometry is known to 
not store any other information.
      * In current Apache SIS usage, this method is invoked only for newly 
created geometries.
      *
-     * <p>This method also sets the JTS SRID to EPSG code if such code can be 
found. For performance reasons
-     * this method does not perform a full scan of EPSG database if the CRS 
does not provide an EPSG code.</p>
+     * <p>This method also sets the <abbr>JTS</abbr> <abbr>SRID</abbr> to the 
<abbr>EPSG</abbr> code if found.
+     * For performance reasons, this method does not perform a full scan of 
EPSG database if the CRS does not
+     * provide an EPSG code.</p>
      *
      * @param  target  the geometry where to store coordinate reference system 
information.
      * @param  crs     the CRS to store, or {@code null}.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index 1f3e98f04c..b7ded1be96 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
@@ -49,6 +49,7 @@ import org.apache.sis.referencing.CRS;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.wrapper.Dimensions;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryType;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
@@ -141,17 +142,16 @@ final class Wrapper extends GeometryWrapper {
     }
 
     /**
-     * Returns the dimension of the coordinates that define this geometry.
+     * Returns an identification of which dimensions are for the coordinates 
of this geometry.
+     *
+     * @return the coordinate dimensions (never {@code null}).
      */
     @Override
-    public int getCoordinateDimension() {
-        int dimension = CRS.getDimensionOrZero(crs);
-        if (dimension == 0) {
-            final var walker = new GeometryWalker();
-            walker.scan(geometry);
-            dimension = walker.dimension();
-        }
-        return dimension;
+    public Dimensions getCoordinateDimensions() {
+        // Should be reasonably quick when the range of Z and M values is not 
requested.
+        final var walker = new GeometryWalker();
+        walker.scan(geometry);
+        return walker.dimensions();
     }
 
     /**
@@ -167,11 +167,11 @@ final class Wrapper extends GeometryWrapper {
         if (crs != null) {
             env = new GeneralEnvelope(crs);
             if (env.getDimension() >= Factory.TRIDIMENSIONAL) {
-                walker.wantZM();
+                walker.wantRangeOfZM();
             }
             walker.scan(geometry);
         } else {
-            walker.wantZM();
+            walker.wantRangeOfZM();
             walker.scan(geometry);
             env = new GeneralEnvelope(walker.dimension());
         }
@@ -372,7 +372,7 @@ add:    for (Geometry next = geometry;;) {
     }
 
     /**
-     * Applies a SQLMM operation on this geometry.
+     * Applies a <abbr>SQLMM</abbr> operation on this geometry.
      *
      * @param  operation  the SQLMM operation to apply.
      * @param  other      the other geometry, or {@code null} if the operation 
requires only one geometry.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/DimensionsTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/DimensionsTest.java
new file mode 100644
index 0000000000..4cc6178994
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/DimensionsTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.geometry.wrapper;
+
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.CRS;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.referencing.crs.HardCodedCRS;
+import org.apache.sis.test.TestCase;
+
+
+/**
+ * Tests {@link Dimensions}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class DimensionsTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public DimensionsTest() {
+    }
+
+    /**
+     * Tests {@link Dimensions#forCRS(CoordinateReferenceSystem)}.
+     */
+    @Test
+    public void testForCRS() {
+        assertSame(Dimensions.XY,   Dimensions.forCRS(HardCodedCRS.WGS84));
+        assertSame(Dimensions.XYZ,  Dimensions.forCRS(HardCodedCRS.WGS84_3D));
+        assertSame(Dimensions.XYZM, Dimensions.forCRS(HardCodedCRS.WGS84_4D));
+        assertSame(Dimensions.XYZ,  Dimensions.forCRS(HardCodedCRS.GEOID_3D));
+        assertSame(Dimensions.XYZM, Dimensions.forCRS(HardCodedCRS.GEOID_4D));
+        assertSame(Dimensions.XYM,  
Dimensions.forCRS(HardCodedCRS.WGS84_WITH_TIME));
+    }
+
+    /**
+     * Tests {@link Dimensions#forZorM(boolean, boolean)}.
+     */
+    @Test
+    public void testForZorM() {
+        assertSame(Dimensions.XY,   Dimensions.forZorM(false, false));
+        assertSame(Dimensions.XYZ,  Dimensions.forZorM(true,  false));
+        assertSame(Dimensions.XYM,  Dimensions.forZorM(false, true));
+        assertSame(Dimensions.XYZM, Dimensions.forZorM(true,  true));
+    }
+
+    /**
+     * Tests {@link Dimensions#forCount(boolean, boolean)}.
+     */
+    @Test
+    public void testForCount() {
+        assertSame(Dimensions.XY,   Dimensions.forCount(2, false));
+        assertSame(Dimensions.XYZ,  Dimensions.forCount(3, false));
+        assertSame(Dimensions.XYM,  Dimensions.forCount(3, true));
+        assertSame(Dimensions.XYZM, Dimensions.forCount(4, true));
+        assertSame(Dimensions.XYZM, Dimensions.forCount(4, false));
+        assertNotNull(assertThrows(IllegalArgumentException.class, () -> 
Dimensions.forCount(1, false)).getMessage());
+    }
+
+    /**
+     * Tests {@link Dimensions#adjust adjust(…)}.
+     *
+     * @throws FactoryException if an error occurred while querying the 
authority factory or creating a <abbr>CRS</abbr>.
+     */
+    @Test
+    public void testAdjust() throws FactoryException {
+        assertSame(HardCodedCRS.WGS84,    Dimensions.XY 
.adjust(HardCodedCRS.WGS84,    null, null, null));
+        assertSame(HardCodedCRS.WGS84,    Dimensions.XY 
.adjust(HardCodedCRS.GEOID_3D, null, null, null));
+        assertSame(HardCodedCRS.WGS84,    Dimensions.XY 
.adjust(HardCodedCRS.GEOID_4D, null, null, null));
+        assertSame(HardCodedCRS.GEOID_3D, 
Dimensions.XYZ.adjust(HardCodedCRS.GEOID_3D, null, null, null));
+
+        CRS.equivalent(HardCodedCRS.WGS84,    Dimensions.XY 
.adjust(HardCodedCRS.WGS84_3D, null, null, null));
+        CRS.equivalent(HardCodedCRS.WGS84_3D, 
Dimensions.XYZ.adjust(HardCodedCRS.WGS84,    null, null, null));
+        CRS.equivalent(HardCodedCRS.GEOID_3D, 
Dimensions.XYZ.adjust(HardCodedCRS.GEOID_4D, null, null, null));
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index 75e33b9e7f..2c1ae978c8 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -35,11 +35,9 @@ import org.opengis.geometry.Envelope;
 import org.opengis.geometry.Geometry;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.cs.CartesianCS;
-import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.EllipsoidalCS;
 import org.opengis.referencing.cs.CoordinateSystem;
-import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.CompoundCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -52,7 +50,6 @@ import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.datum.Datum;
 import org.opengis.referencing.datum.GeodeticDatum;
-import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.OperationNotFoundException;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.TransformException;
@@ -75,20 +72,12 @@ import 
org.apache.sis.referencing.internal.shared.AxisDirections;
 import org.apache.sis.referencing.internal.shared.EllipsoidalHeightCombiner;
 import org.apache.sis.referencing.internal.shared.ReferencingUtilities;
 import org.apache.sis.referencing.internal.shared.DefinitionVerifier;
-import org.apache.sis.referencing.cs.AxisFilter;
-import org.apache.sis.referencing.cs.CoordinateSystems;
-import org.apache.sis.referencing.cs.DefaultVerticalCS;
-import org.apache.sis.referencing.crs.DefaultGeographicCRS;
-import org.apache.sis.referencing.crs.DefaultProjectedCRS;
-import org.apache.sis.referencing.crs.DefaultVerticalCRS;
 import org.apache.sis.referencing.crs.DefaultCompoundCRS;
-import org.apache.sis.referencing.crs.DefaultEngineeringCRS;
 import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.referencing.datum.DefaultPrimeMeridian;
 import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
 import org.apache.sis.referencing.operation.CoordinateOperationContext;
 import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
-import org.apache.sis.referencing.operation.DefiningConversion;
 import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 import org.apache.sis.referencing.factory.UnavailableFactoryException;
 import org.apache.sis.coordinate.DefaultCoordinateMetadata;
@@ -100,6 +89,7 @@ import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.Classes;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.internal.shared.Constants;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
@@ -1146,12 +1136,6 @@ public final class CRS {
      * @since 1.6
      */
     public static Optional<GeodeticDatum> 
getGeodeticReferenceFrame(CoordinateReferenceSystem crs) {
-        if (crs instanceof DerivedCRS) {
-            crs = ((DerivedCRS) crs).getBaseCRS();
-        }
-        if (crs instanceof GeodeticCRS) {
-            return Optional.ofNullable(DatumOrEnsemble.asDatum((GeodeticCRS) 
crs));
-        }
         if (crs instanceof CompoundCRS) {
             for (CoordinateReferenceSystem component : ((CompoundCRS) 
crs).getComponents()) {
                 final Optional<GeodeticDatum> datum = 
getGeodeticReferenceFrame(component);
@@ -1160,6 +1144,19 @@ public final class CRS {
                 }
             }
         }
+        for (;;) {
+            // Need to be tested before the base CRS because a derived CRS can 
also be geodetic.
+            if (crs instanceof GeodeticCRS) {
+                return 
Optional.ofNullable(DatumOrEnsemble.asDatum((GeodeticCRS) crs));
+            }
+            if (crs instanceof DerivedCRS) {
+                // Paranoic check for avoiding never-ending loop.
+                if (crs != (crs = ((DerivedCRS) crs).getBaseCRS())) {
+                    continue;
+                }
+            }
+            break;
+        }
         return Optional.empty();
     }
 
@@ -1229,7 +1226,7 @@ public final class CRS {
             final CoordinateReferenceSystem crs = components[0];
             if (crs != null) return crs;
         }
-        return new EllipsoidalHeightCombiner().createCompoundCRS(components);
+        return new 
EllipsoidalHeightCombiner(null).createCompoundCRS(components);
     }
 
     /**
@@ -1351,7 +1348,7 @@ public final class CRS {
          *
          * @param  components  value of {@code getSingleComponents(crs)}.
          * @param  search      the elements to search in the {@code 
components} list.
-         * @param  overwrite   whether to overwrite the {@code search} array 
for matching elements only once.
+         * @param  overwrite   whether to overwrite the {@code search} array 
for matching each element only once.
          * @return mask with indexes of dimensions where a {@code search} 
element is leniently found.
          */
         final BitSet locateDimensions(final List<SingleCRS> components, final 
SingleCRS[] search, final boolean overwrite) {
@@ -1776,24 +1773,18 @@ public final class CRS {
              * or a vertical component. Extracting the vertical component is 
illegal according ISO 19111,
              * but sometime useful as temporary information.
              */
-            GeodeticCRS baseCRS = null;
-            if (crs instanceof GeodeticCRS) {
-                baseCRS = (GeodeticCRS) crs;
-            } else if (crs instanceof ProjectedCRS) {
-                baseCRS = ((ProjectedCRS) crs).getBaseCRS();
-            }
             RuntimeException cause = null;
-            if (baseCRS != null) {
-                int i = lower;
+            if (horizontalCode(crs) != 0) {
                 int dimension = 0;              // Number of dimensions.
                 boolean isVertical = false;     // Whether the requested 
dimension is the vertical one.
+                int i = lower;
                 do {
                     dimension++;
                     isVertical |= 
AxisDirections.isVertical(crs.getCoordinateSystem().getAxis(i).getDirection());
                     i = mask.nextSetBit(i + 1);
                 } while (i >= 0 && i < limit);
                 if (dimension == (isVertical ? 1 : BIDIMENSIONAL)) try {
-                    components.add(new EllipsoidalHeightSeparator(baseCRS, 
isVertical).separate((SingleCRS) crs));
+                    components.add(new 
EllipsoidalHeightSeparator(isVertical).separate((SingleCRS) crs));
                     return false;
                 } catch (IllegalArgumentException | ClassCastException e) {
                     cause = e;
@@ -1829,12 +1820,13 @@ public final class CRS {
 
     /**
      * Returns {@code true} if the given CRS is horizontal. The current 
implementation considers a
-     * CRS as horizontal if it is two-dimensional and comply with one of the 
following conditions:
+     * CRS as horizontal if it is two-dimensional and complies with one of the 
following conditions:
      *
      * <ul>
      *   <li>is an instance of {@link GeographicCRS} (or an equivalent {@link 
GeodeticCRS}), or</li>
      *   <li>is an instance of {@link ProjectedCRS}, or</li>
-     *   <li>is an instance of {@link EngineeringCRS} (following 
<abbr>WKT</abbr> 2 definition of {@literal <horizontal crs>}).</li>
+     *   <li>is an instance of {@link EngineeringCRS} with axis directions 
such as North, South, East or West
+     *       (derived from <abbr>WKT</abbr> 2 definition of {@literal 
<horizontal crs>}).</li>
      * </ul>
      *
      * In case of doubt, this method conservatively returns {@code false}.
@@ -1856,29 +1848,36 @@ public final class CRS {
     }
 
     /**
-     * If the given CRS would qualify as horizontal except for its number of 
dimensions, returns that number.
-     * Otherwise returns 0. The number of dimensions can only be 2 or 3.
+     * If the given <abbr>CRS</abbr> qualifies as horizontal except for its 
number of dimensions, returns that number.
+     * Otherwise returns 0. The number of dimensions can only be 2 or 3 and 
the third dimension can only be vertical.
      */
     private static int horizontalCode(final CoordinateReferenceSystem crs) {
-        /*
-         * In order to determine if the CRS is geographic, checking the 
CoordinateSystem type is more reliable
-         * then checking if the CRS implements the GeographicCRS interface.  
This is because the GeographicCRS
-         * type did not existed in ISO 19111:2007, so a CRS could be 
standard-compliant without implementing
-         * the GeographicCRS interface.
-         */
         boolean isEngineering = false;
         final boolean isGeodetic = (crs instanceof GeodeticCRS);
         if (isGeodetic || crs instanceof ProjectedCRS || (isEngineering = (crs 
instanceof EngineeringCRS))) {
+            /*
+             * In order to determine if a geodetic CRS is geographic, checking 
the CoordinateSystem type is more
+             * reliable then checking if the CRS implements the GeographicCRS 
interface, because `GeographicCRS`
+             * type did not existed in ISO 19111:2007. A CRS can be 
standard-compliant without implementing the
+             * `GeographicCRS` interface.
+             */
             final CoordinateSystem cs = crs.getCoordinateSystem();
             final int dim = cs.getDimension();
-            if ((dim & ~1) == BIDIMENSIONAL && (!isGeodetic || (cs instanceof 
EllipsoidalCS))) {
+            if (dim >= BIDIMENSIONAL && dim <= TRIDIMENSIONAL && (!isGeodetic 
|| (cs instanceof EllipsoidalCS))) {
                 if (isEngineering) {
-                    int n = 0;
+                    int count = 0;
                     for (int i=0; i<dim; i++) {
-                        if 
(AxisDirections.isCompass(cs.getAxis(i).getDirection())) n++;
+                        final AxisDirection direction = 
cs.getAxis(i).getDirection();
+                        if (AxisDirections.isCompass(direction)) {
+                            count++;
+                        } else if (!AxisDirections.isVertical(direction)) {
+                            return 0;
+                        }
                     }
                     // If we don't have exactly 2 east, north, etc. 
directions, consider as non-horizontal.
-                    if (n != BIDIMENSIONAL) return 0;
+                    if (count != BIDIMENSIONAL) {
+                        return 0;
+                    }
                 }
                 return dim;
             }
@@ -1913,14 +1912,14 @@ public final class CRS {
     }
 
     /**
-     * Returns the first horizontal coordinate reference system found in the 
given CRS, or {@code null} if there is
-     * none. If the given CRS is already horizontal according {@link 
#isHorizontalCRS(CoordinateReferenceSystem)},
-     * then this method returns it as-is. Otherwise if the given CRS is 
compound, then this method searches for the
-     * first horizontal component in the {@linkplain 
#getSingleComponents(CoordinateReferenceSystem)
-     * list of single components}.
+     * Returns the first horizontal component found in the given 
<abbr>CRS</abbr>, or {@code null} if none.
+     * If the given <abbr>CRS</abbr> is already horizontal according
+     * {@link #isHorizontalCRS(CoordinateReferenceSystem)}, then this method 
returns it as-is.
+     * Otherwise if the given CRS is compound, this method searches for the 
first horizontal component
+     * in the {@linkplain #getSingleComponents(CoordinateReferenceSystem) list 
of single components}.
      *
-     * <p>In the special case where a three-dimensional geographic or 
projected CRS is found, this method
-     * will create a two-dimensional geographic or projected CRS without the 
vertical axis.</p>
+     * <p>In the special case where a three-dimensional geographic or 
projected <abbr>CRS</abbr> is found,
+     * this method creates a two-dimensional geographic or projected CRS 
without the vertical axis.</p>
      *
      * @param  crs  the coordinate reference system, or {@code null}.
      * @return the first horizontal CRS, or {@code null} if none.
@@ -1935,53 +1934,20 @@ public final class CRS {
             /*
              * If the CRS is already two-dimensional and horizontal, return 
as-is.
              * We don't need to check if crs is an instance of SingleCRS since 
all
-             * CRS accepted by horizontalCode(…) are SingleCRS.
+             * CRS accepted by `horizontalCode(…)` are `SingleCRS`.
              */
             case BIDIMENSIONAL: {
                 return (SingleCRS) crs;
             }
             case TRIDIMENSIONAL: {
-                /*
-                 * The CRS would be horizontal if we can remove the vertical 
axis. CoordinateSystems.replaceAxes(…)
-                 * will do this task for us. We can verify if the operation 
has been successful by checking that
-                 * the number of dimensions has been reduced by 1 (from 3 to 
2).
-                 */
-                final CoordinateSystem cs = 
CoordinateSystems.replaceAxes(crs.getCoordinateSystem(), new AxisFilter() {
-                    @Override public boolean accept(final CoordinateSystemAxis 
axis) {
-                        return !AxisDirections.isVertical(axis.getDirection());
+                try {
+                    final SingleCRS candidate = new 
EllipsoidalHeightSeparator(false).separate((SingleCRS) crs);
+                    if (getDimensionOrZero(candidate) == BIDIMENSIONAL) {
+                        return candidate;
                     }
-                });
-                if (cs.getDimension() != BIDIMENSIONAL) break;
-                /*
-                 * Most of the time, the CRS to rebuild will be geodetic. In 
such case we known that the
-                 * coordinate system is ellipsoidal because (i.e. the CRS is 
geographic) because it was
-                 * a condition verified by horizontalCode(…). A 
ClassCastException would be a bug.
-                 */
-                final Map<String, ?> properties = 
ReferencingUtilities.getPropertiesForModifiedCRS(crs);
-                if (crs instanceof GeodeticCRS) {
-                    final var source = (GeodeticCRS) crs;
-                    return new DefaultGeographicCRS(properties, 
source.getDatum(), source.getDatumEnsemble(), (EllipsoidalCS) cs);
+                } catch (FactoryException e) {
+                    throw new BackingStoreException(e);
                 }
-                /*
-                 * In Apache SIS implementation, the Conversion contains the 
source and target CRS together with
-                 * a MathTransform.   We need to recreate the same conversion, 
but without CRS and MathTransform
-                 * for letting SIS create or associate new ones, which will be 
two-dimensional now.
-                 */
-                if (crs instanceof ProjectedCRS) {
-                    final var proj = (ProjectedCRS) crs;
-                    final var base = (GeodeticCRS) 
getHorizontalComponent(proj.getBaseCRS());
-                    Conversion fromBase = proj.getConversionFromBase();
-                    fromBase = new DefiningConversion(
-                            IdentifiedObjects.getProperties(fromBase),
-                            fromBase.getMethod(), null,
-                            fromBase.getParameterValues());
-                    return new DefaultProjectedCRS(properties, base, fromBase, 
(CartesianCS) cs);
-                }
-                /*
-                 * If the CRS is neither geographic or projected, then it is 
engineering.
-                 */
-                final var source = (EngineeringCRS) crs;
-                return new DefaultEngineeringCRS(properties, 
source.getDatum(), source.getDatumEnsemble(), cs);
             }
         }
         if (crs instanceof CompoundCRS) {
@@ -1996,12 +1962,12 @@ public final class CRS {
     }
 
     /**
-     * Returns the first vertical coordinate reference system found in the 
given CRS, or {@code null} if there is none.
+     * Returns the first vertical component found in the given 
<abbr>CRS</abbr>, or {@code null} if none.
      * If the given CRS is already an instance of {@code VerticalCRS}, then 
this method returns it as-is.
      * Otherwise if the given CRS is compound, then this method searches for 
the first vertical component
      * in the {@linkplain #getSingleComponents(CoordinateReferenceSystem) list 
of single components}.
      *
-     * <h4>Height in a three-dimensional geographic CRS</h4>
+     * <h4>Height in a three-dimensional geographic <abbr>CRS</abbr></h4>
      * In ISO 19111 model, ellipsoidal heights are indissociable from 
geographic CRS because such heights
      * without their (<var>latitude</var>, <var>longitude</var>) locations 
make little sense. Consequently
      * a standard-conformant library should return {@code null} when asked for 
the {@code VerticalCRS}
@@ -2033,10 +1999,10 @@ public final class CRS {
             return (VerticalCRS) crs;
         }
         if (crs instanceof CompoundCRS) {
-            final CompoundCRS cp = (CompoundCRS) crs;
+            final var compound = (CompoundCRS) crs;
             boolean a = false;
             do { // Executed at most twice.
-                for (final CoordinateReferenceSystem c : cp.getComponents()) {
+                for (final CoordinateReferenceSystem c : 
compound.getComponents()) {
                     final VerticalCRS candidate = getVerticalComponent(c, a);
                     if (candidate != null) {
                         return candidate;
@@ -2045,23 +2011,17 @@ public final class CRS {
             } while ((a = !a) == allowCreateEllipsoidal);
         }
         if (allowCreateEllipsoidal && horizontalCode(crs) == TRIDIMENSIONAL) {
-            final CoordinateSystem cs = crs.getCoordinateSystem();
-            final int i = AxisDirections.indexOfColinear(cs, AxisDirection.UP);
-            if (i >= 0) {
-                final CoordinateSystemAxis axis = cs.getAxis(i);
-                VerticalCRS c = CommonCRS.Vertical.ELLIPSOIDAL.crs();
-                if (!c.getCoordinateSystem().getAxis(0).equals(axis)) {
-                    final Map<String,?> properties = 
IdentifiedObjects.getProperties(c);
-                    c = new DefaultVerticalCRS(properties, c.getDatum(), 
c.getDatumEnsemble(), new DefaultVerticalCS(properties, axis));
-                }
-                return c;
+            try {
+                return (VerticalCRS) new 
EllipsoidalHeightSeparator(true).separate((SingleCRS) crs);
+            } catch (FactoryException e) {
+                throw new BackingStoreException(e);
             }
         }
         return null;
     }
 
     /**
-     * Returns the first temporal coordinate reference system found in the 
given CRS, or {@code null} if there is none.
+     * Returns the first temporal component found in the given 
<abbr>CRS</abbr>, or {@code null} if none.
      * If the given CRS is already an instance of {@code TemporalCRS}, then 
this method returns it as-is.
      * Otherwise if the given CRS is compound, then this method searches for 
the first temporal component
      * in the {@linkplain #getSingleComponents(CoordinateReferenceSystem) list 
of single components}.
@@ -2077,8 +2037,8 @@ public final class CRS {
             return (TemporalCRS) crs;
         }
         if (crs instanceof CompoundCRS) {
-            final CompoundCRS cp = (CompoundCRS) crs;
-            for (final CoordinateReferenceSystem c : cp.getComponents()) {
+            final var compound = (CompoundCRS) crs;
+            for (final CoordinateReferenceSystem c : compound.getComponents()) 
{
                 final TemporalCRS candidate = getTemporalComponent(c);
                 if (candidate != null) {
                     return candidate;
@@ -2088,6 +2048,52 @@ public final class CRS {
         return null;
     }
 
+    /**
+     * Returns the first geospatial component found in the given 
<abbr>CRS</abbr>, or {@code null} if none.
+     * In this method, a geospatial component is defined as a two- or 
three-dimensional <abbr>CRS</abbr>
+     * which {@linkplain #hasHorizontalComponent(CoordinateReferenceSystem) 
has an horizontal component},
+     * optionally combined with a vertical axis (height or depth) as the third 
dimension.
+     *
+     * @param  crs  the coordinate reference system, or {@code null}.
+     * @return the first geospatial CRS, or {@code null} if none.
+     *
+     * @since 1.8
+     */
+    @OptionalCandidate
+    public static CoordinateReferenceSystem getGeospatialComponent(final 
CoordinateReferenceSystem crs) {
+        if (horizontalCode(crs) != 0) {
+            return crs;
+        }
+        CoordinateReferenceSystem candidate = null;
+        if (crs instanceof CompoundCRS) {
+            final var compound = (CompoundCRS) crs;
+            VerticalCRS vertical = null;
+            boolean other = false;
+            for (final CoordinateReferenceSystem c : compound.getComponents()) 
{
+                if (candidate == null && (candidate = 
getGeospatialComponent(c)) != null) {
+                    other |= (candidate != c);
+                } else if (vertical == null && (vertical = 
getVerticalComponent(c, false)) != null) {
+                    other |= (vertical != c);
+                } else {
+                    other = true;
+                }
+            }
+            if (!other) {
+                return compound;    // All components are geospatial.
+            } else if (candidate != null && vertical != null) try {
+                /*
+                 * May happen if the compound CRS is (x, y, z, t) and we need 
to keep only (x, y, z).
+                 * But should rarely happen in practice, because SIS prefers 
nested compound CRS such
+                 * as ((x, y, z), t), which can be separated without building 
new CRS.
+                 */
+                return compound(candidate, vertical);
+            } catch (FactoryException ex) {
+                throw new BackingStoreException(ex);
+            }
+        }
+        return candidate;
+    }
+
     /**
      * Returns the ordered list of single coordinate reference systems for the 
specified CRS.
      * This method performs the following choices:
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
index 51be90ee78..7dba93b0be 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
@@ -307,7 +307,7 @@ public enum CommonCRS {
            (short) 0, (short) 0, (short) 0, (short) 0, (byte) 0, (byte) 0);    
             // UPS and UTM info
 
     /**
-     * The enum for the default CRS.
+     * The enumeration value for the default <abbr>CRS</abbr>.
      *
      * @see #defaultGeographic()
      */
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
index 9e30dca2f4..de23e9e09b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EllipsoidalHeightSeparator.java
@@ -20,6 +20,7 @@ import org.opengis.util.FactoryException;
 import org.opengis.referencing.cs.VerticalCS;
 import org.opengis.referencing.cs.CartesianCS;
 import org.opengis.referencing.cs.EllipsoidalCS;
+import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.crs.CRSFactory;
@@ -27,11 +28,14 @@ import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeodeticCRS;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.apache.sis.referencing.internal.shared.AxisDirections;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.cs.AxisFilter;
+import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.referencing.operation.DefiningConversion;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.resources.Errors;
@@ -43,7 +47,7 @@ import org.opengis.referencing.datum.DatumEnsemble;
 
 
 /**
- * Helper class for separating the ellipsoidal height from the horizontal part 
of a CRS.
+ * Helper class for separating the ellipsoidal height from the horizontal part 
of a <abbr>CRS</abbr>.
  * This is the converse of {@link 
org.apache.sis.referencing.internal.shared.EllipsoidalHeightCombiner}.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -51,30 +55,17 @@ import org.opengis.referencing.datum.DatumEnsemble;
  * @see org.apache.sis.referencing.internal.shared.EllipsoidalHeightCombiner
  */
 final class EllipsoidalHeightSeparator implements AxisFilter {
-    /**
-     * The value of {@link SingleCRS#getDatum()}.
-     */
-    private final GeodeticDatum datum;
-
-    /**
-     * The datum ensemble of the <abbr>CRS</abbr> to separate, or {@code null} 
if none.
-     */
-    private final DatumEnsemble<GeodeticDatum> ensemble;
-
     /**
      * Whether to extract the vertical component ({@code true}) or the 
horizontal component ({@code false}).
      */
     private final boolean vertical;
 
     /**
-     * Creates a new separator for a CRS having the given base.
+     * Creates a new separator.
      *
-     * @param  baseCRS   the CRS to separate, or the base CRS of the projected 
CRS to separate.
      * @param  vertical  whether to extract the vertical component ({@code 
true}) or the horizontal component ({@code false}).
      */
-    EllipsoidalHeightSeparator(final GeodeticCRS baseCRS, final boolean 
vertical) {
-        this.datum    = baseCRS.getDatum();
-        this.ensemble = baseCRS.getDatumEnsemble();
+    EllipsoidalHeightSeparator(final boolean vertical) {
         this.vertical = vertical;
     }
 
@@ -83,7 +74,8 @@ final class EllipsoidalHeightSeparator implements AxisFilter {
      */
     @Override
     public boolean accept(final CoordinateSystemAxis axis) {
-        return AxisDirections.isVertical(axis.getDirection()) == vertical;
+        final AxisDirection direction = axis.getDirection();
+        return vertical ? AxisDirections.isVertical(direction) : 
AxisDirections.isCompass(direction);
     }
 
     /**
@@ -104,7 +96,11 @@ final class EllipsoidalHeightSeparator implements 
AxisFilter {
      * @throws ClassCastException if a coordinate system is not of the 
expected type.
      */
     SingleCRS separate(final SingleCRS crs) throws FactoryException {
-        final CoordinateSystem cs = 
CoordinateSystems.replaceAxes(crs.getCoordinateSystem(), this);
+        final CoordinateSystem sourceCS = crs.getCoordinateSystem();
+        final CoordinateSystem cs = CoordinateSystems.replaceAxes(sourceCS, 
this);
+        if (cs == sourceCS) {
+            return crs;
+        }
         if (vertical) {
             VerticalCRS component = CommonCRS.Vertical.ELLIPSOIDAL.crs();
             if 
(!Utilities.equalsIgnoreMetadata(component.getCoordinateSystem(), cs)) {
@@ -124,26 +120,33 @@ final class EllipsoidalHeightSeparator implements 
AxisFilter {
                 throw new IllegalArgumentException(Errors.format(
                         Errors.Keys.UnsupportedCoordinateSystem_1, 
IdentifiedObjects.getName(cs, null)));
             }
-            final CommonCRS ref = CommonCRS.WGS84;
-            if 
(Utilities.equalsIgnoreMetadata(ref.geographic().getCoordinateSystem(), cs)) {
-                final CommonCRS c = CommonCRS.forDatum(datum, ensemble);
-                if (c != null) return c.geographic();
-            } else if 
(Utilities.equalsIgnoreMetadata(ref.normalizedGeographic().getCoordinateSystem(),
 cs)) {
-                final CommonCRS c = CommonCRS.forDatum(datum, ensemble);
-                if (c != null) return c.normalizedGeographic();
+            final var source = (GeodeticCRS) crs;
+            final GeodeticDatum datum = source.getDatum();
+            final DatumEnsemble<GeodeticDatum> ensemble = 
source.getDatumEnsemble();
+            final CommonCRS predefined = CommonCRS.forDatum(datum, ensemble);
+            if (predefined != null && 
predefined.datum(true).equals(DatumOrEnsemble.asDatum(source))) {
+                // We require an equal datum because a datum change in this 
operation would be confusing.
+                boolean normalized = false;
+                do {
+                    GeographicCRS candidate = normalized ? 
predefined.normalizedGeographic() : predefined.geographic();
+                    if 
(Utilities.equalsIgnoreMetadata(candidate.getCoordinateSystem(), cs)) {
+                        return candidate;
+                    }
+                } while ((normalized = !normalized) == true);
             }
-            return 
factory().createGeographicCRS(getPropertiesForModifiedCRS(crs), datum, 
ensemble, (EllipsoidalCS) cs);
+            return 
factory().createGeographicCRS(getPropertiesForModifiedCRS(source), datum, 
ensemble, (EllipsoidalCS) cs);
         }
         /*
          * In the projected CRS case, in addition of reducing the number of 
dimensions in the CartesianCS,
          * we also need to reduce the number of dimensions in the base CRS and 
in the conversion.
          */
         if (crs instanceof ProjectedCRS) {
-            GeodeticCRS baseCRS = ((ProjectedCRS) crs).getBaseCRS();
+            final var source = (ProjectedCRS) crs;
+            GeodeticCRS baseCRS = source.getBaseCRS();
             if (CRS.getDimensionOrZero(baseCRS) != CRS.BIDIMENSIONAL) {
                 baseCRS = (GeodeticCRS) separate(baseCRS);
             }
-            Conversion projection = ((ProjectedCRS) 
crs).getConversionFromBase();
+            Conversion projection = source.getConversionFromBase();
             /*
              * The conversion object of the given CRS has a base (source) CRS 
and a target CRS that are not
              * the ones of the new `ProjectedCRS` to create. In addition it 
has a `MathTransform` expecting
@@ -156,7 +159,15 @@ final class EllipsoidalHeightSeparator implements 
AxisFilter {
                     getPropertiesForModifiedCRS(projection),
                     projection.getMethod(), null,
                     projection.getParameterValues());
-            return 
factory().createProjectedCRS(getPropertiesForModifiedCRS(crs), baseCRS, 
projection, (CartesianCS) cs);
+            return 
factory().createProjectedCRS(getPropertiesForModifiedCRS(source), baseCRS, 
projection, (CartesianCS) cs);
+        }
+        /*
+         * Horizontal engineering CRS requested. The axis directions have 
already been verified by `accept(…)`.
+         */
+        if (crs instanceof EngineeringCRS) {
+            final var source = (EngineeringCRS) crs;
+            return factory().createEngineeringCRS(
+                    getPropertiesForModifiedCRS(source), source.getDatum(), 
source.getDatumEnsemble(), cs);
         }
         throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, 
crs.getClass()));
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
index ebfa40973a..657acc513f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
@@ -616,7 +616,8 @@ public final class IdentifiedObjects {
      * Looks up an <abbr>EPSG</abbr> code, such as {@code 4326}, of the 
specified object.
      * This method searches in <abbr>EPSG</abbr> factories known to 
<abbr>SIS</abbr> for
      * an object {@linkplain org.apache.sis.util.ComparisonMode#APPROXIMATE 
approximately equals}
-     * to the specified object. If such an object is found, then its EPSG 
identifier is returned.
+     * to the given object. If such an object is found and has an 
<abbr>EPSG</abbr> identifier,
+     * that identifier is returned.
      * Otherwise or if there is ambiguity, this method returns {@code null}.
      *
      * <p><strong>Note that this method checks the identifier 
validity.</strong>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
index 03fe99a4e0..41a5fcae6a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
@@ -148,8 +148,8 @@ final class StandardDefinitions {
      * @param  world  {@code true} if the properties shall have an entry for 
the domain of validity.
      * @return the map of properties to give to constructors or factory 
methods.
      */
-    private static Map<String,Object> properties(final int code, final String 
name, final String alias, final boolean world) {
-        final var map = new HashMap<String,Object>(8);
+    private static Map<String, Object> properties(final int code, final String 
name, final String alias, final boolean world) {
+        final var map = new HashMap<String, Object>(8);
         if (code != 0) {
             map.put(IDENTIFIERS_KEY, new NamedIdentifier(AUTHORITY, 
Constants.EPSG, String.valueOf(code), VERSION, null));
         }
@@ -168,7 +168,7 @@ final class StandardDefinitions {
      * Adds to the given properties an additional identifier in the {@code 
"CRS"} namespace.
      * This method presumes that the only identifier that existed before this 
method call was the EPSG one.
      */
-    private static void addWMS(final Map<String,Object> properties, final 
String code) {
+    private static void addWMS(final Map<String, Object> properties, final 
String code) {
         properties.put(IDENTIFIERS_KEY, new NamedIdentifier[] {
             (NamedIdentifier) properties.get(IDENTIFIERS_KEY),
             new NamedIdentifier(Citations.WMS, Constants.OGC, code, null, null)
@@ -310,7 +310,7 @@ final class StandardDefinitions {
             case 7048: name  = "GRS 1980 Authalic Sphere"; semiMajorAxis = 
other = AUTHALIC_RADIUS; ivfDefinitive = false; break;
             default:   throw new AssertionError(code);
         }
-        final Map<String,Object> map = properties(code, name, alias, false);
+        final Map<String, Object> map = properties(code, name, alias, false);
         if (ivfDefinitive) {
             return DefaultEllipsoid.createFlattenedSphere(map, semiMajorAxis, 
other, unit);
         } else {
@@ -323,7 +323,7 @@ final class StandardDefinitions {
      * If another prime meridian is desired, the EPSG database shall be used.
      */
     static PrimeMeridian primeMeridian() {
-        final var properties = new HashMap<String,Object>(4);
+        final var properties = new HashMap<String, Object>(4);
         properties.put(NAME_KEY, new NamedIdentifier(AUTHORITY, "Greenwich")); 
         // Name is fixed by ISO 19111.
         properties.put(IDENTIFIERS_KEY, new NamedIdentifier(AUTHORITY, 
Constants.EPSG, GREENWICH, VERSION, null));
         return new DefaultPrimeMeridian(properties, 0, Units.DEGREE);
@@ -358,7 +358,7 @@ final class StandardDefinitions {
                        break;
             default:   throw new AssertionError(code);
         }
-        Map<String,Object> properties = properties(csCode, csName, null, 
false);
+        Map<String, Object> properties = properties(csCode, csName, null, 
false);
         final var cs = new DefaultVerticalCS(properties, createAxis(axis, 
true));
         properties = properties(code, name, alias, true);
         if (wms != null) {
@@ -446,10 +446,10 @@ final class StandardDefinitions {
             case CARTESIAN_2D:   name = "Cartesian 2D";                       
axisCode =    3; break;
             case UPS_NORTH:      name = "Cartesian 2D for UPS north";         
axisCode = 1067; break;
             case UPS_SOUTH:      name = "Cartesian 2D for UPS south";         
axisCode = 1059; break;
-            default:   if (!mandatory) return null;
-                       throw new AssertionError(code);
+            default: if (!mandatory) return null;
+                     throw new AssertionError(code);
         }
-        final Map<String,?> properties = properties(code, name, null, false);
+        final Map<String, ?> properties = properties(code, name, null, false);
         CoordinateSystemAxis xAxis = null, yAxis = null, zAxis = null;
         switch (dim) {
             default: throw new AssertionError(dim);
@@ -575,7 +575,7 @@ final class StandardDefinitions {
             default:   if (!mandatory) return null;
                        throw new AssertionError(code);
         }
-        final Map<String,Object> properties = properties(code, name, null, 
false);
+        final Map<String, Object> properties = properties(code, name, null, 
false);
         properties.put(DefaultCoordinateSystemAxis.MINIMUM_VALUE_KEY, min);
         properties.put(DefaultCoordinateSystemAxis.MAXIMUM_VALUE_KEY, max);
         properties.put(DefaultCoordinateSystemAxis.RANGE_MEANING_KEY, rm);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/Codes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/Codes.java
index 2d4ba20552..d8d8d45688 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/Codes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/Codes.java
@@ -24,13 +24,14 @@ import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.apache.sis.measure.Units;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.internal.shared.Constants;
 import static org.apache.sis.util.internal.shared.Constants.EPSG_METRE;
 import static org.apache.sis.util.internal.shared.Constants.EPSG_AXIS_DEGREES;
 
 
 /**
  * Map units of measurement and axis directions to {@link CoordinateSystem} 
objects defined in the EPSG database.
- * Current version uses hard-coded mapping.
+ * Current version uses hard-coded mapping. The mapping correctness is 
verified by {@code CodesTest}.
  *
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -38,20 +39,23 @@ import static 
org.apache.sis.util.internal.shared.Constants.EPSG_AXIS_DEGREES;
 final class Codes {
     /**
      * Axis directions as a maximum of 4 directions packed in a single integer.
-     * Each byte is an {@link AxisDirection#ordinal()} value.
+     * Each byte is an {@link AxisDirection#ordinal()} value + 1 with 0 
meaning no axis in that dimension.
+     * First axis is in the lowest byte.
      *
+     * @see #getDimension()
      * @see #pack(AxisDirection[])
      */
-    final int directions;
+    private final int directions;
 
     /**
-     * EPSG code of the target unit of measurement.
+     * <abbr>EPSG</abbr> code of the target unit of measurement.
      * Applies only to the two first axes. The third axis, if any, is assumed 
in metres.
      */
-    final short unit;
+    private final short unit;
 
     /**
-     * EPSG code of the coordinate reference system.
+     * <abbr>EPSG</abbr> code of the coordinate reference system.
+     * Interpreted as an unsigned integer.
      */
     final short epsg;
 
@@ -64,6 +68,13 @@ final class Codes {
         this.epsg       = epsg;
     }
 
+    /**
+     * Returns the number of dimensions of the coordinate system represented 
by this code.
+     */
+    final int getDimension() {
+        return 4 - (Integer.numberOfLeadingZeros(directions) >>> 3);
+    }
+
     /**
      * Packs the given axis directions in a single integer.
      *
@@ -75,10 +86,10 @@ final class Codes {
         if (i <= Integer.BYTES) {
             while (--i >= 0) {
                 final int ordinal = directions[i].ordinal();
-                if (ordinal < 0 || ordinal > Byte.MAX_VALUE) {
+                if (ordinal < 0 || ordinal > Byte.MAX_VALUE - 1) {
                     return OptionalInt.empty();
                 }
-                packed = (packed << Byte.SIZE) | ordinal;
+                packed = (packed << Byte.SIZE) | (ordinal + 1);
             }
         }
         return OptionalInt.of(packed);
@@ -89,8 +100,9 @@ final class Codes {
      *
      * @param  unit  the unit of the two first axis. The third axis, if any, 
is assumed in metres.
      * @param  directions  axis directions to look for.
+     * @return the <abbr>EPSG</abbr> code, or 0 if none.
      */
-    static short lookup(final Unit<?> unit, final AxisDirection[] directions) {
+    static int lookup(final Unit<?> unit, final AxisDirection[] directions) {
         final Integer uc = Units.getEpsgCode(unit, true);
         if (uc != null) {
             final OptionalInt p = pack(directions);
@@ -98,7 +110,7 @@ final class Codes {
                 Codes m = new Codes(p.getAsInt(), uc.shortValue(), (short) 0);
                 m = EPSG.get(m);
                 if (m != null) {
-                    return m.epsg;
+                    return Short.toUnsignedInt(m.epsg);
                 }
             }
         }
@@ -128,12 +140,21 @@ final class Codes {
     }
 
     /**
-     * All hard-coded EPSG codes known to this class.
+     * Returns a string representation for debugging purposes.
      */
-    private static final Map<Codes,Codes> EPSG = new HashMap<>(31);
+    @Override
+    public String toString() {
+        return Constants.EPSG + ':' + Short.toUnsignedInt(epsg) + " (" + 
getDimension() + "D)";
+    }
+
+    /**
+     * All hard-coded <abbr>EPSG</abbr> codes known to this class.
+     * The mapping correctness is verified by {@code CodesTest}.
+     */
+    private static final Map<Codes, Codes> EPSG = new HashMap<>(31);
     static {
         final var directions = new AxisDirection[] {AxisDirection.EAST, 
AxisDirection.NORTH};
-        final int addVertical = AxisDirection.UP.ordinal() << (2 * Byte.SIZE);
+        final int addVertical = (AxisDirection.UP.ordinal() + 1) << (2 * 
Byte.SIZE);
         OptionalInt packed = pack(directions);
         short unit = EPSG_METRE;
 loop:   for (int i=0; ; i++) {
@@ -171,18 +192,14 @@ loop:   for (int i=0; ; i++) {
             }
             final int p = packed.getAsInt();
             Codes m = new Codes(p, unit, epsg);
-            if (EPSG.put(m, m) != null) {
-                throw new AssertionError(m.epsg);
-            }
+            EPSG.put(m, m);
             /*
              * Add three-dimensional cases. The vertical axis is in metres
              * (this is encoded in CodesTest.VERTICAL_UNIT constant).
              */
             if (to3D != 0) {
                 m = new Codes(p | addVertical, unit, to3D);
-                if (EPSG.put(m, m) != null) {
-                    throw new AssertionError(m.epsg);
-                }
+                EPSG.put(m, m);
             }
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
index 855a23a2bc..8bf714361d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/CoordinateSystems.java
@@ -39,6 +39,7 @@ import org.apache.sis.measure.ElevationAngle;
 import org.apache.sis.measure.Units;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.internal.shared.DoubleDouble;
 import org.apache.sis.referencing.internal.Resources;
@@ -56,7 +57,7 @@ import 
org.apache.sis.referencing.operation.matrix.UnderdeterminedMatrixExceptio
  * between two coordinate systems.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.8
  * @since   0.4
  */
 public final class CoordinateSystems {
@@ -648,18 +649,19 @@ next:   for (final CoordinateSystem cs : targets) {
     }
 
     /**
-     * Returns the EPSG code of a coordinate system using the units and 
directions of given axes.
+     * Returns the <abbr>EPSG</abbr> code of a coordinate system using the 
units and directions of given axes.
      * This method ignores axis metadata (names, abbreviation, identifiers, 
remarks, <i>etc.</i>).
      * The axis minimum and maximum values are checked only if the
      * {@linkplain CoordinateSystemAxis#getRangeMeaning() range meaning} is 
"wraparound".
-     * If no suitable coordinate system is known to Apache SIS, then this 
method returns {@code null}.
+     * If no suitable coordinate system is known to Apache <abbr>SIS</abbr>, 
then this method returns {@code null}.
      *
-     * <p>Current implementation uses a hard-coded list of known coordinate 
systems;
-     * it does not yet scan the EPSG database (this may change in future 
Apache SIS version).
+     * <p>The current implementation uses a hard-coded list of known 
coordinate systems.
+     * It does not yet scan the <abbr>EPSG</abbr> database content
+     * (this may change in future Apache <abbr>SIS</abbr> version).
      * The current list of known coordinate systems is given below.</p>
      *
      * <table class="sis">
-     *   <caption>Known coordinate systems (CS)</caption>
+     *   <caption>Known coordinate systems (<abbr>CS</abbr>)</caption>
      *   <tr><th>EPSG</th> <th>CS type</th> <th colspan="3">Axis 
directions</th> <th>Horizontal unit</th></tr>
      *   <tr><td>6424</td> <td>Ellipsoidal</td> <td>east</td>  <td>north</td> 
<td></td>   <td>degree</td></tr>
      *   <tr><td>6422</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td>  
<td></td>   <td>degree</td></tr>
@@ -686,65 +688,58 @@ next:   for (final CoordinateSystem cs : targets) {
      *   <tr><td>4497</td> <td>Cartesian</td>   <td>east</td>  <td>north</td> 
<td></td>   <td>US survey foot</td></tr>
      * </table>
      *
-     * @param  type  the type of coordinate system for which an EPSG code is 
desired, as a GeoAPI interface.
-     * @param  axes  axes for which a coordinate system EPSG code is desired.
-     * @return EPSG codes for a coordinate system using the given axes 
(ignoring metadata), or {@code null} if unknown
-     *         to this method. Note that a null value does not mean that a 
more  extensive search in the EPSG database
-     *         would not find a matching coordinate system.
+     * @param  type  the type of coordinate system for which an 
<abbr>EPSG</abbr> code is desired.
+     * @param  axes  axes for which a coordinate system <abbr>EPSG</abbr> code 
is desired.
+     * @return <abbr>EPSG</abbr> code for a coordinate system using the given 
axes (ignoring metadata),
+     *         or {@code null} if unknown to this method. Note that a null 
value does not mean that a
+     *         more extensive search in the EPSG database would not find a 
matching coordinate system.
      *
      * @see 
org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createCoordinateSystem(String)
      *
      * @since 1.0
      */
-    @SuppressWarnings("fallthrough")
+    @OptionalCandidate
     public static Integer getEpsgCode(final Class<? extends CoordinateSystem> 
type, final CoordinateSystemAxis... axes) {
         ArgumentChecks.ensureNonNull("type", type);
-forDim: switch (axes.length) {
-            case 3: {
-                if (!Units.METRE.equals(axes[2].getUnit())) break;      // 
Restriction in our hard-coded list of codes.
-                // Fall through
-            }
-            case 2: {
-                final Unit<?> unit = axes[0].getUnit();
-                if (unit != null && unit.equals(axes[1].getUnit())) {
-                    final boolean isAngular = Units.isAngular(unit);
-                    if ((isAngular && 
type.isAssignableFrom(EllipsoidalCS.class)) ||
-                         Units.isLinear(unit) && 
type.isAssignableFrom(CartesianCS.class))
-                    {
-                        /*
-                         * Current implementation defines EPSG codes for 
EllipsoidalCS and CartesianCS only.
-                         * Those two coordinate system types can be 
differentiated by the unit of the two first axes.
-                         * If a future implementation supports more CS types, 
above condition will need to be updated.
-                         */
-                        final var directions = new AxisDirection[axes.length];
-                        for (int i=0; i<directions.length; i++) {
-                            final CoordinateSystemAxis axis = axes[i];
-                            ArgumentChecks.ensureNonNullElement("axes", i, 
axis);
-                            directions[i] = axis.getDirection();
-                            if (isAngular && axis.getRangeMeaning() == 
RangeMeaning.WRAPAROUND) try {
-                                final UnitConverter uc = 
unit.getConverterToAny(Units.DEGREE);
-                                final double min = 
uc.convert(axis.getMinimumValue());
-                                final double max = 
uc.convert(axis.getMaximumValue());
-                                if ((min > Double.NEGATIVE_INFINITY && 
Math.abs(min - Longitude.MIN_VALUE) > Formulas.ANGULAR_TOLERANCE) ||
-                                    (max < Double.POSITIVE_INFINITY && 
Math.abs(max - Longitude.MAX_VALUE) > Formulas.ANGULAR_TOLERANCE))
-                                {
-                                    break forDim;
-                                }
-                            } catch (IncommensurableException e) {      // 
Should never happen since we checked that units are angular.
-                                Logging.unexpectedException(AbstractCS.LOGGER, 
CoordinateSystems.class, "getEpsgCode", e);
-                                break forDim;
+        if (axes.length == 2 || (axes.length == 3 && 
Units.METRE.equals(axes[2].getUnit()))) {
+            final Unit<?> unit = axes[0].getUnit();
+            if (unit != null && unit.equals(axes[1].getUnit())) try {
+                final boolean isAngular = Units.isAngular(unit);
+                if ((isAngular && EllipsoidalCS.class.isAssignableFrom(type)) 
||
+                    (Units.isLinear(unit) && 
CartesianCS.class.isAssignableFrom(type)))
+                {
+                    /*
+                     * Current implementation defines EPSG codes for 
EllipsoidalCS and CartesianCS only.
+                     * Those two coordinate system types can be differentiated 
by the unit of the two first axes.
+                     * If a future implementation supports more CS types, 
above condition will need to be updated.
+                     */
+                    final var directions = new AxisDirection[axes.length];
+                    for (int i=0; i<directions.length; i++) {
+                        final CoordinateSystemAxis axis = axes[i];
+                        ArgumentChecks.ensureNonNullElement("axes", i, axis);
+                        directions[i] = axis.getDirection();
+                        if (isAngular && axis.getRangeMeaning() == 
RangeMeaning.WRAPAROUND) {
+                            final UnitConverter uc = 
unit.getConverterToAny(Units.DEGREE);
+                            final double min = 
uc.convert(axis.getMinimumValue());
+                            final double max = 
uc.convert(axis.getMaximumValue());
+                            if ((min > Double.NEGATIVE_INFINITY && 
Math.abs(min - Longitude.MIN_VALUE) > Formulas.ANGULAR_TOLERANCE) ||
+                                (max < Double.POSITIVE_INFINITY && 
Math.abs(max - Longitude.MAX_VALUE) > Formulas.ANGULAR_TOLERANCE))
+                            {
+                                return null;
                             }
                         }
-                        return getEpsgCode(unit, directions);
                     }
+                    return getEpsgCode(unit, directions);
                 }
+            } catch (IncommensurableException e) {      // Should never happen 
since we checked that units are angular.
+                Logging.unexpectedException(AbstractCS.LOGGER, 
CoordinateSystems.class, "getEpsgCode", e);
             }
         }
         return null;
     }
 
     /**
-     * Returns the EPSG code of a coordinate system using the given unit and 
axis directions.
+     * Returns the <abbr>EPSG</abbr> code of a coordinate system using the 
given unit and axis directions.
      * This convenience method performs the work documented in {@link 
#getEpsgCode(Class, CoordinateSystemAxis...)},
      * but requiring only a frequently used subset of information.
      * If no suitable coordinate system is known to Apache SIS, then this 
method returns {@code null}.
@@ -766,6 +761,7 @@ forDim: switch (axes.length) {
      *
      * @since 0.8
      */
+    @OptionalCandidate
     public static Integer getEpsgCode(final Unit<?> unit, final 
AxisDirection... directions) {
         ArgumentChecks.ensureNonNull("unit", unit);
         ArgumentChecks.ensureNonNull("directions", directions);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
index 3dc9ba1a87..30bb93df0a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
@@ -33,7 +33,7 @@
  * and units between two coordinate systems, or filtering axes.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.7
+ * @version 1.8
  * @since   0.4
  */
 @XmlSchema(location = 
"http://schemas.opengis.net/gml/3.2.1/coordinateSystems.xsd";,
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombiner.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombiner.java
index 9cdba31c4c..a8ee20c8ee 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombiner.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombiner.java
@@ -18,12 +18,13 @@ package org.apache.sis.referencing.internal.shared;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.logging.Logger;
 import org.opengis.util.FactoryException;
 import org.opengis.metadata.extent.Extent;
+import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.GeodeticCRS;
-import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.cs.CSFactory;
@@ -35,10 +36,17 @@ import org.opengis.referencing.datum.VerticalDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.CoordinateOperationFactory;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.datum.DatumOrEnsemble;
+import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.metadata.iso.extent.Extents;
+import org.apache.sis.system.Loggers;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.internal.shared.Constants;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.referencing.ObjectDomain;
@@ -58,20 +66,13 @@ public final class EllipsoidalHeightCombiner {
      */
     private final ReferencingFactoryContainer factories;
 
-    /**
-     * Creates a new combiner with no initial factory.
-     */
-    public EllipsoidalHeightCombiner() {
-        factories = new ReferencingFactoryContainer();
-    }
-
     /**
      * Creates a new instance initialized to the factories of the given 
container.
      *
-     * @param  c  the container from which to fetch the factories.
+     * @param  c  the container from which to fetch the factories, or {@code 
null} for the default factories.
      */
     public EllipsoidalHeightCombiner(final ReferencingFactoryContainer c) {
-        factories = c;
+        factories = (c != null) ? c : new ReferencingFactoryContainer();
     }
 
     /**
@@ -108,7 +109,7 @@ public final class EllipsoidalHeightCombiner {
      * @return the coordinate reference system for the given properties.
      * @throws FactoryException if the object creation failed.
      */
-    public final CoordinateReferenceSystem createCompoundCRS(final 
Map<String,?> properties,
+    public final CoordinateReferenceSystem createCompoundCRS(final Map<String, 
?> properties,
             CoordinateReferenceSystem... components) throws FactoryException
     {
         for (int i=0; i<components.length; i++) {
@@ -130,30 +131,41 @@ public final class EllipsoidalHeightCombiner {
                         axisPosition = 1;
                     }
                     /*
-                     * At this point we have the horizontal and vertical 
components. The horizontal component
-                     * begins at 'axisPosition', which is almost always zero. 
Create the three-dimensional CRS.
-                     * If the result is the CRS to be returned directly by 
this method (components.length == 2),
-                     * use the properties given in argument. Otherwise we need 
to use other properties; current
-                     * implementation recycles the properties of the existing 
two-dimensional CRS.
+                     * At this point, we have the horizontal and vertical 
components. The horizontal component
+                     * begins at `axisPosition`, which is almost always zero 
but following code accepts also 1.
+                     * Try to get a 3D coordinate system from the EPSG 
database if possible because it provides
+                     * better metadata (in particular a more appropriate CS 
name).
                      */
-                    final CoordinateSystemAxis[] axes = new 
CoordinateSystemAxis[3];
+                    final var axes = new CoordinateSystemAxis[3];
                     axes[axisPosition++   ] = cs2D.getAxis(0);
                     axes[axisPosition++   ] = cs2D.getAxis(1);
                     axes[axisPosition %= 3] = 
vertical.getCoordinateSystem().getAxis(0);
-                    final Map<String,?> csProps  = 
IdentifiedObjects.getProperties(cs2D, CoordinateSystem.IDENTIFIERS_KEY);
-                    final Map<String,?> crsProps = (components.length == 2) ? 
properties
-                                                 : 
IdentifiedObjects.getProperties(crs, CoordinateReferenceSystem.IDENTIFIERS_KEY);
+                    final String epsgCode = getEpsgCode(cs2D, axes);
+                    /*
+                     * If the 3D CRS created in this block will be the CRS to 
return, the `properties` given in
+                     * argument to this method are used directly. Otherwise we 
need to derive something else.
+                     * Current implementation reuses the properties of the 
two-dimensional CRS.
+                     */
+                    final Map<String, ?> crsProps = (components.length == 2)
+                            ? properties : 
ReferencingUtilities.getPropertiesForModifiedCRS(crs);
                     if (crs instanceof GeodeticCRS) {
                         final var geod = (GeodeticCRS) crs;
-                        final EllipsoidalCS cs3D;
-                        cs3D = factories.getCSFactory() 
.createEllipsoidalCS(csProps, axes[0], axes[1], axes[2]);
-                        crs  = 
factories.getCRSFactory().createGeographicCRS(crsProps, geod.getDatum(), 
geod.getDatumEnsemble(), cs3D);
-                    } else {
-                        final ProjectedCRS proj = (ProjectedCRS) crs;
+                        EllipsoidalCS cs3D = null;
+                        if (epsgCode != null) try {
+                            cs3D = 
factories.getCSAuthorityFactory().createEllipsoidalCS(epsgCode);
+                        } catch (NoSuchAuthorityCodeException e) {
+                            warning(e);
+                        }
+                        if (cs3D == null) {
+                            cs3D = 
factories.getCSFactory().createEllipsoidalCS(
+                                    
ReferencingUtilities.getPropertiesForModifiedCRS(cs2D), axes[0], axes[1], 
axes[2]);
+                        }
+                        crs = 
factories.getCRSFactory().createGeographicCRS(crsProps, geod.getDatum(), 
geod.getDatumEnsemble(), cs3D);
+                    } else if (crs instanceof ProjectedCRS) {
+                        final var proj = (ProjectedCRS) crs;
                         GeodeticCRS base = proj.getBaseCRS();
                         if (base.getCoordinateSystem().getDimension() == 2) {
-                            base = (GeodeticCRS) createCompoundCRS(
-                                    IdentifiedObjects.getProperties(base, 
GeographicCRS.IDENTIFIERS_KEY), base, vertical);
+                            base = (GeodeticCRS) 
createCompoundCRS(ReferencingUtilities.getPropertiesForModifiedCRS(base), base, 
vertical);
                         }
                         /*
                          * In Apache SIS implementation, the Conversion 
contains the source and target CRS together with
@@ -165,9 +177,20 @@ public final class EllipsoidalHeightCombiner {
                                     IdentifiedObjects.getProperties(fromBase),
                                     fromBase.getMethod(), 
fromBase.getParameterValues());
 
-                        final CartesianCS cs3D;
-                        cs3D = factories.getCSFactory() 
.createCartesianCS(csProps, axes[0], axes[1], axes[2]);
+                        CartesianCS cs3D = null;
+                        if (epsgCode != null) try {
+                            cs3D = 
factories.getCSAuthorityFactory().createCartesianCS(epsgCode);
+                        } catch (NoSuchAuthorityCodeException e) {
+                            warning(e);
+                        }
+                        if (cs3D == null) {
+                            cs3D = factories.getCSFactory().createCartesianCS(
+                                    
ReferencingUtilities.getPropertiesForModifiedCRS(cs2D), axes[0], axes[1], 
axes[2]);
+                        }
                         crs  = 
factories.getCRSFactory().createProjectedCRS(crsProps, base, fromBase, cs3D);
+                    } else {
+                        throw new FactoryException(Errors.format(
+                                Errors.Keys.UnsupportedType_1, 
Classes.getStandardClass(crs, CoordinateReferenceSystem.class)));
                     }
                     /*
                      * Remove the VerticalCRS and store the three-dimensional 
GeographicCRS in place of the previous
@@ -209,6 +232,33 @@ public final class EllipsoidalHeightCombiner {
         return null;
     }
 
+    /**
+     * Returns the authority code for a variant of the given coordinate system 
with the given axes.
+     * For consistency, we search for an authority code only if the given 
<abbr>CS</abbr> was itself
+     * defined by an <abbr>EPSG</abbr>.
+     *
+     * @param  base  the coordinate system for which to get a variant.
+     * @param  axes  the axes of the desired coordinate system.
+     * @return a variant of the given coordinate system with the given axes.
+     */
+    private static String getEpsgCode(final CoordinateSystem base, final 
CoordinateSystemAxis[] axes) {
+        if (IdentifiedObjects.getIdentifier(base, Citations.EPSG) != null) {
+            final Integer epsgCode = 
CoordinateSystems.getEpsgCode(base.getClass(), axes);
+            if (epsgCode != null) {
+                return Constants.EPSG + ':' + epsgCode;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Logs a warnings for a non-fatal exception occurring while creating a 
coordinate system.
+     */
+    private static void warning(final NoSuchAuthorityCodeException e) {
+        Logging.recoverableException(Logger.getLogger(Loggers.CRS_FACTORY),
+                EllipsoidalHeightCombiner.class, "createCompoundCRS", e);
+    }
+
     /**
      * Suggests properties for a compound CRS made of the given elements.
      * This method builds a default CRS name and domain of validity.
@@ -216,8 +266,8 @@ public final class EllipsoidalHeightCombiner {
      * @param  components  the components for which to get a default set of 
properties.
      * @return suggested properties in a modifiable map. Callers can modify 
the returned map.
      */
-    static Map<String,Object> properties(final CoordinateReferenceSystem... 
components) {
-        final StringBuilder name = new StringBuilder(40);
+    static Map<String, Object> properties(final CoordinateReferenceSystem[] 
components) {
+        final var name = new StringBuilder(40);
         Extent domain = null;
         for (int i=0; i<components.length; i++) {
             final CoordinateReferenceSystem crs = components[i];
@@ -228,7 +278,7 @@ public final class EllipsoidalHeightCombiner {
                 domain = Extents.intersection(domain, 
obj.getDomainOfValidity());
             }
         }
-        final var properties = new HashMap<String,Object>(4);
+        final var properties = new HashMap<String, Object>(4);
         properties.put(CoordinateReferenceSystem.NAME_KEY, name.toString());
         properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, domain);
         return properties;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
index e745d32268..db0ee9a83a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingFactoryContainer.java
@@ -134,7 +134,7 @@ public class ReferencingFactoryContainer implements 
Localized {
      * The default properties, or an empty map if none. This map shall not be 
modified since its
      * reference may be shared without cloning. This field will be cleared 
when no longer needed.
      */
-    private Map<String,?> defaultProperties;
+    private Map<String, ?> defaultProperties;
 
     /**
      * Creates a new instance for the default factories.
@@ -150,7 +150,7 @@ public class ReferencingFactoryContainer implements 
Localized {
      *
      * @param  properties  the factories.
      */
-    public ReferencingFactoryContainer(final Map<String,?> properties) {
+    public ReferencingFactoryContainer(final Map<String, ?> properties) {
         nameFactory      = (NameFactory)                
properties.get(NAME_FACTORY);
         datumFactory     = (DatumFactory)               
properties.get(DATUM_FACTORY);
         csFactory        = (CSFactory)                  
properties.get(CS_FACTORY);
@@ -170,7 +170,7 @@ public class ReferencingFactoryContainer implements 
Localized {
      * @param  operationFactory   the factory to use for creating (defining) 
conversions.
      * @param  mtFactory          the factory to use for creating transform 
objects.
      */
-    public ReferencingFactoryContainer(final Map<String,?>              
defaultProperties,
+    public ReferencingFactoryContainer(final Map<String, ?>             
defaultProperties,
                                        final CRSFactory                 
crsFactory,
                                        final CSFactory                  
csFactory,
                                        final DatumFactory               
datumFactory,
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
index 45fc3bb4c9..688ba5aed1 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
@@ -103,7 +103,7 @@ public final class ReferencingUtilities {
     public static Unit<?> getUnit(final CoordinateSystem cs) {
         Unit<?> unit = null;
         if (cs != null) {
-            for (int i=cs.getDimension(); --i>=0;) {
+            for (int i = cs.getDimension(); --i >= 0;) {
                 final CoordinateSystemAxis axis = cs.getAxis(i);
                 if (axis != null) {                                         // 
Paranoiac check.
                     final Unit<?> candidate = axis.getUnit();
@@ -147,10 +147,7 @@ public final class ReferencingUtilities {
      * @see org.apache.sis.referencing.internal.VerticalDatumTypes#ELLIPSOIDAL
      */
     public static boolean isEllipsoidalHeight(final VerticalDatum datum) {
-        if (datum != null) {
-            return 
datum.getRealizationMethod().map(VerticalDatumTypes::ellipsoidal).orElse(false);
-        }
-        return false;
+        return (datum != null) && 
datum.getRealizationMethod().filter(VerticalDatumTypes::ellipsoidal).isPresent();
     }
 
     /**
@@ -247,7 +244,7 @@ public final class ReferencingUtilities {
      * @return the identified object properties.
      */
     public static Map<String,?> getPropertiesWithoutIdentifiers(final 
IdentifiedObject object, final Map<String,?> overwrite) {
-        final Map<String,?> properties = 
IdentifiedObjects.getProperties(object, IdentifiedObject.IDENTIFIERS_KEY);
+        final Map<String, ?> properties = 
IdentifiedObjects.getProperties(object, IdentifiedObject.IDENTIFIERS_KEY);
         final Identifier name = object.getName();
         final boolean keepName = name.getCodeSpace() == null && 
name.getAuthority() == null;
         if (keepName && overwrite == null) {
@@ -287,8 +284,8 @@ public final class ReferencingUtilities {
      *
      * @see IdentifiedObjects#getProperties(IdentifiedObject, String...)
      */
-    public static Map<String,?> getPropertiesForModifiedCRS(final 
IdentifiedObject object) {
-        final Map<String,?> properties = 
IdentifiedObjects.getProperties(object, IdentifiedObject.IDENTIFIERS_KEY);
+    public static Map<String, ?> getPropertiesForModifiedCRS(final 
IdentifiedObject object) {
+        final Map<String, ?> properties = 
IdentifiedObjects.getProperties(object, IdentifiedObject.IDENTIFIERS_KEY);
         final Identifier id = (Identifier) 
properties.get(IdentifiedObject.NAME_KEY);
         if (id != null) {
             String name = id.getCode();
@@ -339,7 +336,7 @@ public final class ReferencingUtilities {
             if (spec == Specification.ISO_19111) {
                 final String name = uml.identifier();
                 final int length = name.length();
-                final StringBuilder buffer = new 
StringBuilder(length).append(name, name.indexOf('_') + 1, length);
+                final var buffer = new StringBuilder(length).append(name, 
name.indexOf('_') + 1, length);
                 if (buffer.length() != 0) {
                     buffer.setCharAt(0, 
Character.toLowerCase(buffer.charAt(0)));
                     return buffer;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 68dab41148..bd07c5647b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -1085,7 +1085,7 @@ public class CoordinateOperationFinder extends 
CoordinateOperationRegistry {
     private CoordinateReferenceSystem createCompoundCRS(final 
CoordinateReferenceSystem template,
             final CoordinateReferenceSystem[] components) throws 
FactoryException
     {
-        EllipsoidalHeightCombiner c = new 
EllipsoidalHeightCombiner(factorySIS.crsFactory, factorySIS.csFactory, factory);
+        final var c = new EllipsoidalHeightCombiner(factorySIS.crsFactory, 
factorySIS.csFactory, factory);
         CoordinateReferenceSystem crs = 
c.createCompoundCRS(derivedFrom(template), components);
         return toAuthorityDefinition(CoordinateReferenceSystem.class, crs);
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index b963d0f8d0..0cc6ac4479 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -1254,7 +1254,7 @@ class CoordinateOperationRegistry {
                 return candidate;               // Keep the existing instance 
since it may contain useful metadata.
             }
         }
-        final EllipsoidalHeightCombiner c = new 
EllipsoidalHeightCombiner(factorySIS.crsFactory, factorySIS.csFactory, factory);
+        final var c = new EllipsoidalHeightCombiner(factorySIS.crsFactory, 
factorySIS.csFactory, factory);
         return toAuthorityDefinition(CoordinateReferenceSystem.class, 
c.createCompoundCRS(derivedFrom(crs), crs, 
CommonCRS.Vertical.ELLIPSOIDAL.crs()));
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
index 2158ca67aa..2e918c3f7e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
@@ -331,6 +331,8 @@ public final class CRSTest extends TestCaseWithLogs {
         assertSame(HardCodedCRS.WGS84,                 
CRS.getHorizontalComponent(HardCodedCRS.WGS84));
         assertSame(HardCodedCRS.WGS84_LATITUDE_FIRST,  
CRS.getHorizontalComponent(HardCodedCRS.WGS84_LATITUDE_FIRST));
         assertEqualsIgnoreMetadata(HardCodedCRS.WGS84, 
CRS.getHorizontalComponent(HardCodedCRS.WGS84_3D));
+        assertSame(HardCodedCRS.WGS84,                 
CRS.getHorizontalComponent(HardCodedCRS.GEOID_3D));
+        assertSame(HardCodedCRS.WGS84,                 
CRS.getHorizontalComponent(HardCodedCRS.GEOID_4D));
         loggings.assertNoUnexpectedLog();
     }
 
@@ -346,6 +348,8 @@ public final class CRSTest extends TestCaseWithLogs {
 
         assertSame(HardCodedCRS.ELLIPSOIDAL_HEIGHT,     
CRS.getVerticalComponent(HardCodedCRS.ELLIPSOIDAL_HEIGHT, false));
         assertSame(HardCodedCRS.ELLIPSOIDAL_HEIGHT,     
CRS.getVerticalComponent(HardCodedCRS.ELLIPSOIDAL_HEIGHT, true));
+        assertSame(HardCodedCRS.GRAVITY_RELATED_HEIGHT, 
CRS.getVerticalComponent(HardCodedCRS.GEOID_3D, false));
+        assertSame(HardCodedCRS.GRAVITY_RELATED_HEIGHT, 
CRS.getVerticalComponent(HardCodedCRS.GEOID_3D, true));
         assertSame(HardCodedCRS.GRAVITY_RELATED_HEIGHT, 
CRS.getVerticalComponent(HardCodedCRS.GEOID_4D, false));
         assertSame(HardCodedCRS.GRAVITY_RELATED_HEIGHT, 
CRS.getVerticalComponent(HardCodedCRS.GEOID_4D, true));
 
@@ -370,6 +374,24 @@ public final class CRSTest extends TestCaseWithLogs {
         loggings.assertNoUnexpectedLog();
     }
 
+    /**
+     * Tests {@link CRS#getGeospatialComponent(CoordinateReferenceSystem)}.
+     */
+    @Test
+    public void testGetGeospatialComponent() {
+        assertNull(CRS.getGeospatialComponent(HardCodedCRS.TIME));
+        
assertNull(CRS.getGeospatialComponent(HardCodedCRS.ELLIPSOIDAL_HEIGHT));
+        assertNull(CRS.getGeospatialComponent(HardCodedCRS.GEOCENTRIC));
+
+        assertSame(HardCodedCRS.WGS84,                    
CRS.getGeospatialComponent(HardCodedCRS.WGS84));
+        assertSame(HardCodedCRS.WGS84_LATITUDE_FIRST,     
CRS.getGeospatialComponent(HardCodedCRS.WGS84_LATITUDE_FIRST));
+        assertSame(HardCodedCRS.WGS84_3D,                 
CRS.getGeospatialComponent(HardCodedCRS.WGS84_3D));
+        assertSame(HardCodedCRS.WGS84_3D,                 
CRS.getGeospatialComponent(HardCodedCRS.WGS84_4D));
+        assertSame(HardCodedCRS.GEOID_3D,                 
CRS.getGeospatialComponent(HardCodedCRS.GEOID_3D));
+        assertEqualsIgnoreMetadata(HardCodedCRS.GEOID_3D, 
CRS.getGeospatialComponent(HardCodedCRS.GEOID_4D));
+        loggings.assertNoUnexpectedLog();
+    }
+
     /**
      * Tests getting the horizontal and vertical components of a 
three-dimensional projected CRS.
      */
@@ -516,8 +538,10 @@ public final class CRSTest extends TestCaseWithLogs {
     @Test
     public void testReduceGeographic3D() throws FactoryException {
         final GeographicCRS crs = HardCodedCRS.WGS84_3D;
-        assertSame(CommonCRS.Vertical.ELLIPSOIDAL.crs(),   
CRS.selectDimensions(crs, 2));
-        assertSame(CommonCRS.WGS84.normalizedGeographic(), 
CRS.selectDimensions(crs, 0, 1));
+        assertSame(CommonCRS.Vertical.ELLIPSOIDAL.crs(), 
CRS.selectDimensions(crs, 2));
+        final GeographicCRS horizontal = assertInstanceOf(GeographicCRS.class, 
CRS.selectDimensions(crs, 0, 1));
+        assertEqualsIgnoreMetadata(HardCodedCRS.WGS84, horizontal);
+        assertSame(crs.getDatum(), horizontal.getDatum());
         loggings.assertNoUnexpectedLog();
     }
 
@@ -531,7 +555,9 @@ public final class CRSTest extends TestCaseWithLogs {
     public void testReduceProjected3D() throws FactoryException {
         final ProjectedCRS crs = HardCodedConversions.mercator3D();
         assertSame(CommonCRS.Vertical.ELLIPSOIDAL.crs(), 
CRS.selectDimensions(crs, 2));
-        assertEqualsIgnoreMetadata(HardCodedConversions.mercator(), 
CRS.selectDimensions(crs, 0, 1));
+        final ProjectedCRS horizontal = assertInstanceOf(ProjectedCRS.class, 
CRS.selectDimensions(crs, 0, 1));
+        assertEqualsIgnoreMetadata(HardCodedConversions.mercator(), 
horizontal);
+        assertSame(crs.getDatum(), horizontal.getDatum());
         loggings.assertNoUnexpectedLog();
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/CodesTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/CodesTest.java
index 154cd4e3a2..6c85bd8178 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/CodesTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/cs/CodesTest.java
@@ -17,6 +17,8 @@
 package org.apache.sis.referencing.cs;
 
 import java.util.Map;
+import java.util.Collections;
+import java.util.function.Supplier;
 import java.lang.reflect.Field;
 import javax.measure.Unit;
 import org.opengis.referencing.cs.AxisDirection;
@@ -32,10 +34,11 @@ import org.apache.sis.referencing.factory.TestFactorySource;
 
 
 /**
- * Compares the {@link Codes} elements with the EPSG geodetic database.
+ * Compares the {@link Codes} elements with the <abbr>EPSG</abbr> geodetic 
database.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class CodesTest extends TestCase {
     /**
      * The unit of measurement of the vertical axis.
@@ -49,25 +52,37 @@ public final class CodesTest extends TestCase {
     }
 
     /**
-     * Compares the axis directions and units with EPSG definitions.
+     * Returns the {@link Codes#EPSG} field value.
+     */
+    @SuppressWarnings("unchecked")
+    private static Map<Codes, Codes> codes() throws 
ReflectiveOperationException {
+        final Field field = Codes.class.getDeclaredField("EPSG");
+        field.setAccessible(true);
+        return Collections.unmodifiableMap(((Map<Codes, Codes>) 
field.get(null)));
+    }
+
+    /**
+     * Compares the axis directions and units with <abbr>EPSG</abbr> 
definitions.
      *
      * @throws Exception if an error occurred while fetching the codes or 
querying the database.
      */
     @Test
-    @SuppressWarnings("unchecked")
     public void verify() throws Exception {
+        int count = 0;
         final CSAuthorityFactory factory = 
TestFactorySource.getSharedFactory();
-        final Field field = Codes.class.getDeclaredField("EPSG");
-        field.setAccessible(true);
-        for (final Codes c : ((Map<Codes,?>) field.get(null)).keySet()) {
-            final CoordinateSystem cs = 
factory.createCoordinateSystem(String.valueOf(c.epsg));
+        for (final Codes code : codes().keySet()) {
+            final Supplier<String> message = code::toString;
+            final CoordinateSystem cs = 
factory.createCoordinateSystem(String.valueOf(code.epsg));
             final Unit<?> unit = cs.getAxis(0).getUnit();
-            final AxisDirection[] directions = new 
AxisDirection[cs.getDimension()];
+            final var directions = new AxisDirection[cs.getDimension()];
             for (int i=0; i<directions.length; i++) {
-                assertEquals(i < 2 ? unit : VERTICAL_UNIT, 
cs.getAxis(i).getUnit());
+                assertEquals(i < 2 ? unit : VERTICAL_UNIT, 
cs.getAxis(i).getUnit(), message);
                 directions[i] = cs.getAxis(i).getDirection();
             }
-            assertEquals(c.epsg, Codes.lookup(unit, directions));
+            assertEquals(code.epsg, Codes.lookup(unit, directions), message);
+            assertTrue(code.getDimension() == directions.length);
+            count++;
         }
+        assertEquals(23, count);    // Expected number to be adjusted when 
`Codes` is updated.
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombinerTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombinerTest.java
index 8b0bd5335e..868c8fc531 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombinerTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/shared/EllipsoidalHeightCombinerTest.java
@@ -163,7 +163,11 @@ public final class EllipsoidalHeightCombinerTest extends 
TestCase {
      */
     @Test
     public void testProperties() {
-        final Map<String,?> properties = 
EllipsoidalHeightCombiner.properties(HardCodedCRS.WGS84, 
HardCodedCRS.GRAVITY_RELATED_HEIGHT, HardCodedCRS.TIME);
+        final Map<String, ?> properties = 
EllipsoidalHeightCombiner.properties(new SingleCRS[] {
+            HardCodedCRS.WGS84,
+            HardCodedCRS.GRAVITY_RELATED_HEIGHT,
+            HardCodedCRS.TIME
+        });
         assertEquals("WGS 84 + MSL height + Time", properties.remove("name"));
         assertEquals(Extents.WORLD, properties.remove("domainOfValidity"));
         assertTrue(properties.isEmpty());
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java
index 38aadc6e33..7ef0e868ca 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java
@@ -19,9 +19,11 @@ package org.apache.sis.storage.sql.feature;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.sql.ResultSet;
+import org.opengis.util.FactoryException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.geometry.wrapper.Geometries;
+import org.apache.sis.geometry.wrapper.Dimensions;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.util.resources.Errors;
 
@@ -178,8 +180,45 @@ final class GeometryGetter<G, V extends G> extends 
ValueGetter<V> {
             crs = stmts.fetchCRS(geom.getSRID().orElse(gpkgSrid));
         }
         if (crs != null) {
-            geom.setCoordinateReferenceSystem(crs);
+            /*
+            * The CRS built from the SRID may be two-dimensional while the 
geometry may have z or M coordinates.
+            * For example, a GeoPackage `srs_id` references a horizontal CRS 
even if the `gpkg_geometry_columns`
+            * declares that z values are mandatory. The adjustement below can 
add or remove CRS dimensions.
+            * The vertical (z) dimension, if unspecified, defaults to 
ellipsoidal height (3D CS) in metres.
+            * The measure (M) dimension has no default. If missing, caller may 
have a dimension mismatch.
+             */
+            final Dimensions dimensions = geom.getCoordinateDimensions();
+            Adjusted adjusted = lastAdjusted;
+            if (adjusted == null || (adjusted.source != crs || 
adjusted.dimensions != dimensions)) {
+                lastAdjusted = adjusted = new Adjusted(dimensions, crs);
+            }
+            geom.setCoordinateReferenceSystem(adjusted.target);
         }
         return valueType.cast(geometryFactory.getGeometry(geom));
     }
+
+    /**
+     * The last <abbr>CRS</abbr> computed by an adjustment of the number of 
dimensions, or {@code null} if none.
+     */
+    private volatile Adjusted lastAdjusted;
+
+    /**
+     * The last <abbr>CRS</abbr> computed by an adjustment of the number of 
dimensions.
+     * Cached because all geometries of a column usually have the same 
<abbr>SRID</abbr> and number of dimensions.
+     * Instances are immutable for allowing the use of this cache by 
concurrent threads.
+     */
+    private static final class Adjusted {
+        /** The dimensions used for computing the target <abbr>CRS</abbr>. */
+        final Dimensions dimensions;
+
+        /** The <abbr>CRS</abbr> before and after adjustment for the number of 
dimensions. */
+        final CoordinateReferenceSystem source, target;
+
+        /** Creates a new cache entry. */
+        Adjusted(final Dimensions dimensions, final CoordinateReferenceSystem 
source) throws FactoryException {
+            this.dimensions = dimensions;
+            this.source     = source;
+            this.target     = dimensions.adjust(source, null, null, null);
+        }
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
index 2896b847bd..e4612604a6 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/Wrapper.java
@@ -48,6 +48,7 @@ import org.apache.sis.geometries.operation.GeometryProcessor;
 import org.apache.sis.geometries.surface.Polygon;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.wrapper.Dimensions;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryType;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
@@ -175,21 +176,13 @@ public final class Wrapper extends GeometryWrapper {
     }
 
     /**
-     * Returns the dimension of the coordinates that define this geometry.
-     */
-    @Override
-    public int getCoordinateDimension() {
-        return getCoordinatesDimension(geometry);
-    }
-
-    /**
-     * Gets the number of dimensions of geometry vertex (sequence of 
coordinate tuples), which can be 2 or 3.
+     * Returns an identification of which dimensions are for the coordinates 
of this geometry.
      *
-     * @param  geometry  the geometry for which to get <em>vertex</em> (not 
topological) dimension.
-     * @return vertex dimension of the given geometry.
+     * @todo Implement a more reliable mechanism.
      */
-    private static int getCoordinatesDimension(final Geometry geometry) {
-        return 
geometry.getCoordinateReferenceSystem().getCoordinateSystem().getDimension();
+    @Override
+    public Dimensions getCoordinateDimensions() {
+        return Dimensions.forCRS(geometry.getCoordinateReferenceSystem());
     }
 
     /**

Reply via email to