You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2024/03/07 18:11:54 UTC

(sis) 01/01: Merge branch 'geoapi-3.1'. https://issues.apache.org/jira/browse/SIS-596

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

desruisseaux pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 2b40ccfdc4a3072628ae881bf55b4826dc140ab3
Merge: 4150341710 9048ae55d3
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Thu Mar 7 19:09:19 2024 +0100

    Merge branch 'geoapi-3.1'.
    https://issues.apache.org/jira/browse/SIS-596

 endorsed/build.gradle.kts                          |    6 +-
 .../apache/sis/cloud/aws/s3/ClientFileSystem.java  |    2 +-
 .../main/org/apache/sis/console/AboutCommand.java  |    4 +-
 .../main/org/apache/sis/console/Command.java       |    2 +-
 .../main/org/apache/sis/console/CommandRunner.java |    2 +-
 .../apache/sis/console/FormattedOutputCommand.java |    2 +-
 .../org/apache/sis/console/IdentifierCommand.java  |    2 +-
 .../apache/sis/console/ResourcesDownloader.java    |    2 +-
 .../org/apache/sis/console/TransformCommand.java   |    8 +-
 .../org.apache.sis.feature/main/module-info.java   |   13 +-
 .../org/apache/sis/coverage/CoverageCombiner.java  |    6 +-
 .../org/apache/sis/coverage/SampleDimension.java   |    2 +-
 .../org/apache/sis/coverage/SampleRangeFormat.java |    2 +-
 .../coverage/grid/BandAggregateGridCoverage.java   |    4 +-
 .../coverage/grid/CoordinateOperationFinder.java   |    6 +-
 .../apache/sis/coverage/grid/DefaultEvaluator.java |    8 +-
 .../sis/coverage/grid/DimensionAppender.java       |    2 +-
 .../apache/sis/coverage/grid/DimensionReducer.java |    2 +-
 .../sis/coverage/grid/DimensionalityReduction.java |    2 +-
 .../coverage/grid/FractionalGridCoordinates.java   |    2 +-
 .../org/apache/sis/coverage/grid/GridCoverage.java |    4 +-
 .../apache/sis/coverage/grid/GridCoverage2D.java   |    4 +-
 .../sis/coverage/grid/GridCoverageBuilder.java     |   10 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |    8 +-
 .../apache/sis/coverage/grid/GridDerivation.java   |    2 +-
 .../org/apache/sis/coverage/grid/GridExtent.java   |   10 +-
 .../apache/sis/coverage/grid/GridExtentCRS.java    |    2 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |   18 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    |   12 +-
 .../sis/coverage/grid/ReducedGridCoverage.java     |    2 +-
 .../sis/coverage/grid/ResampledGridCoverage.java   |    6 +-
 .../apache/sis/coverage/grid/SliceGeometry.java    |    8 +-
 .../sis/coverage/grid/j2d/BatchComputedImage.java  |  213 ----
 .../sis/coverage/grid/j2d/ColorModelBuilder.java   |  701 --------------
 .../sis/coverage/grid/j2d/ColorModelFactory.java   |  864 -----------------
 .../sis/coverage/grid/j2d/ColorModelType.java      |  106 --
 .../sis/coverage/grid/j2d/ColorsForRange.java      |  291 ------
 .../sis/coverage/grid/j2d/DeferredProperty.java    |  124 ---
 .../apache/sis/coverage/grid/j2d/FillValues.java   |  208 ----
 .../apache/sis/coverage/grid/j2d/ImageLayout.java  |  438 ---------
 .../sis/coverage/grid/j2d/ImageUtilities.java      |  673 -------------
 .../grid/j2d/MultiBandsIndexColorModel.java        |  314 ------
 .../sis/coverage/grid/j2d/ObservableImage.java     |  288 ------
 .../sis/coverage/grid/j2d/RasterFactory.java       |  314 ------
 .../sis/coverage/grid/j2d/SampleModelFactory.java  |  291 ------
 .../sis/coverage/grid/j2d/ScaledColorModel.java    |  162 ----
 .../sis/coverage/grid/j2d/ScaledColorSpace.java    |  223 -----
 .../sis/coverage/grid/j2d/TileErrorHandler.java    |  120 ---
 .../sis/coverage/grid/j2d/TileOpExecutor.java      | 1019 --------------------
 .../sis/coverage/grid/j2d/TilePlaceholder.java     |  404 --------
 .../apache/sis/coverage/grid/j2d/TiledImage.java   |  274 ------
 .../sis/coverage/grid/j2d/WritableTiledImage.java  |  217 -----
 .../apache/sis/coverage/grid/j2d/package-info.java |   31 -
 .../sis/coverage/internal/CommonDomainFinder.java  |  383 --------
 .../sis/coverage/internal/MultiSourceArgument.java |  619 ------------
 .../sis/coverage/internal/RangeArgument.java       |  395 --------
 .../sis/coverage/internal/SampleDimensions.java    |  179 ----
 .../apache/sis/coverage/internal/package-info.java |    2 +-
 .../sis/coverage/privy/BatchComputedImage.java     |  213 ++++
 .../sis/coverage/privy/ColorModelBuilder.java      |  701 ++++++++++++++
 .../sis/coverage/privy/ColorModelFactory.java      |  864 +++++++++++++++++
 .../apache/sis/coverage/privy/ColorModelType.java  |  106 ++
 .../apache/sis/coverage/privy/ColorsForRange.java  |  291 ++++++
 .../sis/coverage/privy/CommonDomainFinder.java     |  383 ++++++++
 .../sis/coverage/privy/DeferredProperty.java       |  124 +++
 .../org/apache/sis/coverage/privy/FillValues.java  |  208 ++++
 .../org/apache/sis/coverage/privy/ImageLayout.java |  438 +++++++++
 .../apache/sis/coverage/privy/ImageUtilities.java  |  673 +++++++++++++
 .../coverage/privy/MultiBandsIndexColorModel.java  |  314 ++++++
 .../sis/coverage/privy/MultiSourceArgument.java    |  619 ++++++++++++
 .../apache/sis/coverage/privy/ObservableImage.java |  288 ++++++
 .../apache/sis/coverage/privy/RangeArgument.java   |  395 ++++++++
 .../apache/sis/coverage/privy/RasterFactory.java   |  314 ++++++
 .../sis/coverage/privy/SampleDimensions.java       |  179 ++++
 .../sis/coverage/privy/SampleModelFactory.java     |  291 ++++++
 .../sis/coverage/privy/ScaledColorModel.java       |  162 ++++
 .../sis/coverage/privy/ScaledColorSpace.java       |  223 +++++
 .../sis/coverage/privy/TileErrorHandler.java       |  120 +++
 .../apache/sis/coverage/privy/TileOpExecutor.java  | 1019 ++++++++++++++++++++
 .../apache/sis/coverage/privy/TilePlaceholder.java |  404 ++++++++
 .../org/apache/sis/coverage/privy/TiledImage.java  |  274 ++++++
 .../sis/coverage/privy/WritableTiledImage.java     |  217 +++++
 .../apache/sis/coverage/privy/package-info.java    |   30 +
 .../org/apache/sis/feature/AbstractFeature.java    |    2 +-
 .../org/apache/sis/feature/CharacteristicMap.java  |    8 +-
 .../apache/sis/feature/CharacteristicTypeMap.java  |    4 +-
 .../apache/sis/feature/DefaultAssociationRole.java |    2 +-
 .../org/apache/sis/feature/DefaultFeatureType.java |    4 +-
 .../main/org/apache/sis/feature/DenseFeature.java  |    4 +-
 .../org/apache/sis/feature/EnvelopeOperation.java  |    6 +-
 .../apache/sis/feature/ExpressionOperation.java    |    6 +-
 .../main/org/apache/sis/feature/FeatureFormat.java |    4 +-
 .../org/apache/sis/feature/FeatureOperations.java  |    2 +-
 .../sis/feature/GroupAsPolylineOperation.java      |    4 +-
 .../main/org/apache/sis/feature/LinkOperation.java |    2 +-
 .../apache/sis/feature/MultiValuedAssociation.java |    2 +-
 .../apache/sis/feature/MultiValuedAttribute.java   |    4 +-
 .../org/apache/sis/feature/NamedFeatureType.java   |    2 +-
 .../org/apache/sis/feature/SingletonAttribute.java |    2 +-
 .../main/org/apache/sis/feature/SparseFeature.java |    4 +-
 .../apache/sis/feature/StringJoinOperation.java    |    6 +-
 .../sis/feature/builder/AttributeTypeBuilder.java  |    8 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |    2 +-
 .../sis/feature/internal/AttributeConvention.java  |  440 ---------
 .../sis/feature/internal/FeatureExpression.java    |  127 ---
 .../sis/feature/internal/FeatureUtilities.java     |   92 --
 .../apache/sis/feature/internal/InstantList.java   |   80 --
 .../sis/feature/internal/MovingFeatures.java       |  129 ---
 .../apache/sis/feature/internal/package-info.java  |    4 +-
 .../sis/feature/privy/AttributeConvention.java     |  440 +++++++++
 .../sis/feature/privy/FeatureExpression.java       |  127 +++
 .../apache/sis/feature/privy/FeatureUtilities.java |   92 ++
 .../org/apache/sis/feature/privy/InstantList.java  |   80 ++
 .../apache/sis/feature/privy/MovingFeatures.java   |  129 +++
 .../org/apache/sis/feature/privy/package-info.java |   32 +
 .../org/apache/sis/filter/ArithmeticFunction.java  |    4 +-
 .../apache/sis/filter/BinaryGeometryFilter.java    |    2 +-
 .../org/apache/sis/filter/ConvertFunction.java     |    4 +-
 .../apache/sis/filter/DefaultFilterFactory.java    |    1 -
 .../org/apache/sis/filter/FunctionRegister.java    |   63 ++
 .../org/apache/sis/filter/IdentifierFilter.java    |    2 +-
 .../main/org/apache/sis/filter/LeafExpression.java |    2 +-
 .../main/org/apache/sis/filter/LogicalFilter.java  |    4 +-
 .../main/org/apache/sis/filter/Optimization.java   |    2 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |    2 +-
 .../apache/sis/filter/internal/FunctionNames.java  |   89 --
 .../sis/filter/internal/FunctionRegister.java      |   65 --
 .../main/org/apache/sis/filter/internal/Node.java  |    2 +-
 .../sis/filter/internal/SortByComparator.java      |  203 ----
 .../org/apache/sis/filter/internal/Visitor.java    |  328 -------
 .../main/org/apache/sis/filter/internal/XPath.java |  251 -----
 .../org/apache/sis/filter/privy/FunctionNames.java |   89 ++
 .../apache/sis/filter/privy/SortByComparator.java  |  203 ++++
 .../main/org/apache/sis/filter/privy/Visitor.java  |  328 +++++++
 .../main/org/apache/sis/filter/privy/XPath.java    |  251 +++++
 .../org/apache/sis/filter/privy/package-info.java  |   31 +
 .../apache/sis/filter/sqlmm/FunctionWithSRID.java  |    2 +-
 .../main/org/apache/sis/filter/sqlmm/Registry.java |    2 +-
 .../main/org/apache/sis/filter/sqlmm/ST_Point.java |    2 +-
 .../apache/sis/filter/sqlmm/SpatialFunction.java   |    2 +-
 .../org/apache/sis/filter/sqlmm/TwoGeometries.java |    2 +-
 .../apache/sis/geometry/wrapper/Geometries.java    |    2 +-
 .../apache/sis/geometry/wrapper/GeometryType.java  |    2 +-
 .../sis/geometry/wrapper/GeometryWrapper.java      |    2 +-
 .../geometry/wrapper/SpatialOperationContext.java  |    6 +-
 .../apache/sis/geometry/wrapper/esri/Factory.java  |    2 +-
 .../sis/geometry/wrapper/j2d/EmptyShape.java       |    2 +-
 .../apache/sis/geometry/wrapper/j2d/Factory.java   |    6 +-
 .../apache/sis/geometry/wrapper/j2d/FlatShape.java |    2 +-
 .../sis/geometry/wrapper/j2d/MultiPolylines.java   |    2 +-
 .../sis/geometry/wrapper/j2d/ShapeProperties.java  |    2 +-
 .../sis/geometry/wrapper/j2d/ShapeWrapper.java     |    2 +-
 .../apache/sis/geometry/wrapper/j2d/Wrapper.java   |    4 +-
 .../apache/sis/geometry/wrapper/jts/Factory.java   |    2 +-
 .../org/apache/sis/geometry/wrapper/jts/JTS.java   |    2 +-
 .../sis/geometry/wrapper/jts/ShapeAdapter.java     |    4 +-
 .../sis/geometry/wrapper/jts/ShapeConverter.java   |    2 +-
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |    2 +-
 .../main/org/apache/sis/image/AnnotatedImage.java  |    6 +-
 .../org/apache/sis/image/BandAggregateImage.java   |    4 +-
 .../main/org/apache/sis/image/BandSelectImage.java |    8 +-
 .../main/org/apache/sis/image/BandSharing.java     |    2 +-
 .../apache/sis/image/BandedSampleConverter.java    |   14 +-
 .../main/org/apache/sis/image/Colorizer.java       |    4 +-
 .../main/org/apache/sis/image/ComputedImage.java   |    4 +-
 .../main/org/apache/sis/image/DataType.java        |    4 +-
 .../main/org/apache/sis/image/ImageCombiner.java   |    6 +-
 .../main/org/apache/sis/image/ImageProcessor.java  |    6 +-
 .../main/org/apache/sis/image/MaskImage.java       |    6 +-
 .../main/org/apache/sis/image/MaskedImage.java     |   10 +-
 .../org/apache/sis/image/MultiSourceImage.java     |    2 +-
 .../org/apache/sis/image/MultiSourceLayout.java    |   10 +-
 .../org/apache/sis/image/MultiSourcePrefetch.java  |    2 +-
 .../main/org/apache/sis/image/PixelIterator.java   |    4 +-
 .../main/org/apache/sis/image/PlanarImage.java     |    6 +-
 .../sis/image/PositionalConsistencyImage.java      |    2 +-
 .../main/org/apache/sis/image/PrefetchedImage.java |    8 +-
 .../main/org/apache/sis/image/RecoloredImage.java  |    6 +-
 .../main/org/apache/sis/image/ResampledImage.java  |    8 +-
 .../main/org/apache/sis/image/ResamplingGrid.java  |    2 +-
 .../org/apache/sis/image/StatisticsCalculator.java |    2 +-
 .../main/org/apache/sis/image/TileCache.java       |    2 +-
 .../main/org/apache/sis/image/Transferer.java      |    6 +-
 .../main/org/apache/sis/image/Visualization.java   |    8 +-
 .../apache/sis/image/WritableComputedImage.java    |    2 +-
 .../sis/image/processing/CompoundFuture.java       |    2 +-
 .../sis/image/processing/isoline/Fragments.java    |    2 +-
 .../org/apache/sis/index/tree/NodeIterator.java    |    2 +-
 .../main/org/apache/sis/index/tree/PointTree.java  |    2 +-
 .../org/apache/sis/pending/geoapi/filter/Name.java |    2 +-
 .../coverage/grid/ConvertedGridCoverageTest.java   |    2 +-
 .../coverage/grid/DimensionalityReductionTest.java |    2 +-
 .../sis/coverage/grid/GridCoverage2DTest.java      |    2 +-
 .../sis/coverage/grid/GridCoverageBuilderTest.java |    2 +-
 .../sis/coverage/grid/GridDerivationTest.java      |    4 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |    2 +-
 .../coverage/grid/ResampledGridCoverageTest.java   |    6 +-
 .../coverage/grid/TranslatedGridCoverageTest.java  |    2 +-
 .../coverage/grid/j2d/ColorModelBuilderTest.java   |  151 ---
 .../sis/coverage/grid/j2d/ImageLayoutTest.java     |   48 -
 .../sis/coverage/grid/j2d/ImageUtilitiesTest.java  |  235 -----
 .../coverage/grid/j2d/SampleModelFactoryTest.java  |  151 ---
 .../coverage/grid/j2d/ScaledColorSpaceTest.java    |  102 --
 .../sis/coverage/internal/RangeArgumentTest.java   |   94 --
 .../sis/coverage/privy/ColorModelBuilderTest.java  |  151 +++
 .../apache/sis/coverage/privy/ImageLayoutTest.java |   48 +
 .../sis/coverage/privy/ImageUtilitiesTest.java     |  235 +++++
 .../sis/coverage/privy/RangeArgumentTest.java      |   94 ++
 .../sis/coverage/privy/SampleModelFactoryTest.java |  151 +++
 .../sis/coverage/privy/ScaledColorSpaceTest.java   |  102 ++
 .../apache/sis/feature/EnvelopeOperationTest.java  |    2 +-
 .../apache/sis/feature/FeatureMemoryBenchmark.java |    2 +-
 .../apache/sis/feature/FeatureOperationsTest.java  |    2 +-
 .../sis/feature/StringJoinOperationTest.java       |    2 +-
 .../feature/builder/AttributeTypeBuilderTest.java  |    2 +-
 .../feature/builder/FeatureTypeBuilderTest.java    |    2 +-
 .../feature/internal/AttributeConventionTest.java  |  122 ---
 .../sis/feature/privy/AttributeConventionTest.java |  122 +++
 .../apache/sis/feature/test/FeatureComparator.java |    2 +-
 .../org/apache/sis/filter/TemporalFilterTest.java  |    2 +-
 .../test/org/apache/sis/filter/XPathTest.java      |    2 +-
 .../sis/filter/internal/FunctionNamesTest.java     |   67 --
 .../apache/sis/filter/privy/FunctionNamesTest.java |   67 ++
 .../apache/sis/geometry/wrapper/jts/JTSTest.java   |    2 +-
 .../apache/sis/image/BandAggregateImageTest.java   |    4 +-
 .../org/apache/sis/image/BandSelectImageTest.java  |    4 +-
 .../sis/image/BandedSampleConverterTest.java       |    2 +-
 .../org/apache/sis/image/ImageCombinerTest.java    |    2 +-
 .../org/apache/sis/image/InterpolationTest.java    |    4 +-
 .../test/org/apache/sis/image/MaskedImageTest.java |    2 +-
 .../test/org/apache/sis/image/PlanarImageTest.java |    2 +-
 .../org/apache/sis/image/ResampledImageTest.java   |    6 +-
 .../org/apache/sis/image/ResamplingGridTest.java   |    4 +-
 .../apache/sis/image/StatisticsCalculatorTest.java |    2 +-
 .../test/org/apache/sis/image/TiledImageMock.java  |    4 +-
 .../sis/image/processing/isoline/IsolinesTest.java |    2 +-
 .../sis/image/processing/isoline/StepsViewer.java  |    2 +-
 ... => org.apache.sis.util.privy.MetadataServices} |    0
 .../org.apache.sis.metadata/main/module-info.java  |   12 +-
 .../org/apache/sis/metadata/MetadataStandard.java  |    2 +-
 .../org/apache/sis/metadata/MetadataVisitor.java   |    2 +-
 .../sis/metadata/MetadataVisitorException.java     |    2 +-
 .../apache/sis/metadata/ModifiableMetadata.java    |    8 +-
 .../org/apache/sis/metadata/PropertyAccessor.java  |   10 +-
 .../main/org/apache/sis/metadata/PropertyMap.java  |    2 +-
 .../main/org/apache/sis/metadata/Pruner.java       |    2 +-
 .../main/org/apache/sis/metadata/SpecialCases.java |    4 +-
 .../main/org/apache/sis/metadata/StateChanger.java |    6 +-
 .../main/org/apache/sis/metadata/TreeNode.java     |    4 +-
 .../org/apache/sis/metadata/TreeTableView.java     |    4 +-
 .../apache/sis/metadata/internal/AxisNames.java    |  221 -----
 .../sis/metadata/internal/CitationConstant.java    |  238 +++++
 .../apache/sis/metadata/internal/Identifiers.java  |  306 ------
 .../metadata/internal/ImplementationHelper.java    |  304 ------
 .../org/apache/sis/metadata/internal/Merger.java   |  425 --------
 .../apache/sis/metadata/internal/NameMeaning.java  |  238 -----
 .../sis/metadata/internal/NameToIdentifier.java    |  321 ------
 .../sis/metadata/internal/RecordSchemaSIS.java     |    2 +-
 .../sis/metadata/internal/ReferencingServices.java |  314 ------
 .../apache/sis/metadata/internal/Resources.java    |    7 +-
 .../sis/metadata/internal/Resources.properties     |    1 -
 .../sis/metadata/internal/Resources_fr.properties  |    1 -
 .../sis/metadata/internal/ServicesForUtility.java  |   14 +-
 .../sis/metadata/internal/TemporalUtilities.java   |  144 ---
 .../metadata/internal/TransformationAccuracy.java  |   89 --
 .../apache/sis/metadata/internal/package-info.java |    2 +-
 .../iso/DefaultExtendedElementInformation.java     |    6 +-
 .../apache/sis/metadata/iso/DefaultMetadata.java   |    6 +-
 .../org/apache/sis/metadata/iso/ISOMetadata.java   |    6 +-
 .../acquisition/DefaultEnvironmentalRecord.java    |    2 +-
 .../sis/metadata/iso/acquisition/DefaultEvent.java |    4 +-
 .../iso/acquisition/DefaultRequestedDate.java      |    4 +-
 .../iso/acquisition/DefaultRequirement.java        |    4 +-
 .../sis/metadata/iso/citation/Citations.java       |    8 +-
 .../sis/metadata/iso/citation/DefaultCitation.java |    6 +-
 .../metadata/iso/citation/DefaultCitationDate.java |    4 +-
 .../sis/metadata/iso/citation/DefaultContact.java  |    4 +-
 .../iso/citation/DefaultResponsibleParty.java      |    4 +-
 .../metadata/iso/citation/DefaultTelephone.java    |    4 +-
 .../sis/metadata/iso/citation/package-info.java    |    2 +-
 .../sis/metadata/iso/content/DefaultBand.java      |    2 +-
 .../iso/content/DefaultCoverageDescription.java    |    6 +-
 .../DefaultFeatureCatalogueDescription.java        |    4 +-
 .../iso/content/DefaultFeatureTypeInfo.java        |    2 +-
 .../iso/content/DefaultImageDescription.java       |    6 +-
 .../iso/content/DefaultRangeDimension.java         |    2 +-
 .../iso/content/DefaultSampleDimension.java        |    2 +-
 .../sis/metadata/iso/content/package-info.java     |    2 +-
 .../metadata/iso/distribution/DefaultDataFile.java |    2 +-
 .../DefaultDigitalTransferOptions.java             |    4 +-
 .../metadata/iso/distribution/DefaultFormat.java   |    4 +-
 .../metadata/iso/distribution/DefaultMedium.java   |   10 +-
 .../distribution/DefaultStandardOrderProcess.java  |    4 +-
 .../metadata/iso/distribution/package-info.java    |    2 +-
 .../sis/metadata/iso/extent/DefaultExtent.java     |    2 +-
 .../iso/extent/DefaultGeographicBoundingBox.java   |    2 +-
 .../iso/extent/DefaultSpatialTemporalExtent.java   |    4 +-
 .../metadata/iso/extent/DefaultTemporalExtent.java |    4 +-
 .../metadata/iso/extent/DefaultVerticalExtent.java |    2 +-
 .../apache/sis/metadata/iso/extent/Extents.java    |    6 +-
 .../iso/identification/AbstractIdentification.java |    2 +-
 .../DefaultAggregateInformation.java               |    2 +-
 .../iso/identification/DefaultCoupledResource.java |    4 +-
 .../identification/DefaultDataIdentification.java  |    2 +-
 .../DefaultRepresentativeFraction.java             |    6 +-
 .../iso/identification/DefaultResolution.java      |    2 +-
 .../metadata/iso/identification/DefaultUsage.java  |    4 +-
 .../metadata/iso/identification/OperationName.java |    2 +-
 .../metadata/iso/identification/package-info.java  |    2 +-
 .../metadata/iso/legacy/LegacyPropertyAdapter.java |    2 +-
 .../iso/lineage/DefaultNominalResolution.java      |    2 +-
 .../metadata/iso/lineage/DefaultProcessStep.java   |    4 +-
 .../sis/metadata/iso/lineage/DefaultSource.java    |    2 +-
 .../sis/metadata/iso/lineage/package-info.java     |    2 +-
 .../maintenance/DefaultMaintenanceInformation.java |    6 +-
 .../sis/metadata/iso/maintenance/DefaultScope.java |    2 +-
 .../sis/metadata/iso/maintenance/package-info.java |    2 +-
 .../org/apache/sis/metadata/iso/package-info.java  |    2 +-
 .../sis/metadata/iso/quality/AbstractElement.java  |    2 +-
 .../iso/quality/DefaultCoverageResult.java         |    2 +-
 .../metadata/iso/quality/DefaultDataQuality.java   |    2 +-
 .../iso/quality/DefaultEvaluationMethod.java       |    4 +-
 .../iso/quality/DefaultMeasureReference.java       |    2 +-
 .../DefaultNonQuantitativeAttributeAccuracy.java   |    2 +-
 .../iso/quality/DefaultQuantitativeResult.java     |    2 +-
 .../sis/metadata/iso/quality/DefaultUsability.java |    2 +-
 .../sis/metadata/iso/quality/package-info.java     |    2 +-
 .../sis/metadata/iso/spatial/DefaultDimension.java |    2 +-
 .../iso/spatial/DefaultGeometricObjects.java       |    2 +-
 .../spatial/DefaultGridSpatialRepresentation.java  |    2 +-
 .../org/apache/sis/metadata/privy/AxisNames.java   |  221 +++++
 .../org/apache/sis/metadata/privy/Identifiers.java |  306 ++++++
 .../sis/metadata/privy/ImplementationHelper.java   |  304 ++++++
 .../main/org/apache/sis/metadata/privy/Merger.java |  425 ++++++++
 .../org/apache/sis/metadata/privy/NameMeaning.java |  238 +++++
 .../sis/metadata/privy/NameToIdentifier.java       |  321 ++++++
 .../sis/metadata/privy/ReferencingServices.java    |  314 ++++++
 .../sis/metadata/privy/TemporalUtilities.java      |  144 +++
 .../sis/metadata/privy/TransformationAccuracy.java |   90 ++
 .../apache/sis/metadata/privy/package-info.java    |   30 +
 .../sis/metadata/simple/CitationConstant.java      |  237 -----
 .../apache/sis/metadata/simple/SimpleCitation.java |    2 +-
 .../apache/sis/metadata/simple/SimpleDuration.java |    2 +-
 .../apache/sis/metadata/simple/SimpleExtent.java   |    2 +-
 .../apache/sis/metadata/simple/SimpleFormat.java   |    2 +-
 .../metadata/simple/SimpleIdentifiedObject.java    |    4 +-
 .../sis/metadata/simple/SimpleIdentifier.java      |    2 +-
 .../org/apache/sis/metadata/sql/Dispatcher.java    |    4 +-
 .../sis/metadata/sql/IdentifierGenerator.java      |    2 +-
 .../org/apache/sis/metadata/sql/Installer.java     |    2 +-
 .../apache/sis/metadata/sql/MetadataFallback.java  |    2 +-
 .../apache/sis/metadata/sql/MetadataSource.java    |   14 +-
 .../apache/sis/metadata/sql/MetadataWriter.java    |    8 +-
 .../org/apache/sis/metadata/sql/privy/Dialect.java |  149 +++
 .../apache/sis/metadata/sql/privy/Initializer.java |  457 +++++++++
 .../sis/metadata/sql/privy/LocalDataSource.java    |  455 +++++++++
 .../apache/sis/metadata/sql/privy/Reflection.java  |  213 ++++
 .../apache/sis/metadata/sql/privy/SQLBuilder.java  |  367 +++++++
 .../sis/metadata/sql/privy/SQLUtilities.java       |  199 ++++
 .../sis/metadata/sql/privy/ScriptRunner.java       |  836 ++++++++++++++++
 .../org/apache/sis/metadata/sql/privy/Syntax.java  |   81 ++
 .../apache/sis/metadata/sql/privy/TypeMapper.java  |  105 ++
 .../sis/metadata/sql/privy/package-info.java       |   77 ++
 .../org/apache/sis/metadata/sql/util/Dialect.java  |  149 ---
 .../apache/sis/metadata/sql/util/Initializer.java  |  457 ---------
 .../sis/metadata/sql/util/LocalDataSource.java     |  455 ---------
 .../apache/sis/metadata/sql/util/Reflection.java   |  213 ----
 .../apache/sis/metadata/sql/util/SQLBuilder.java   |  367 -------
 .../apache/sis/metadata/sql/util/SQLUtilities.java |  199 ----
 .../apache/sis/metadata/sql/util/ScriptRunner.java |  836 ----------------
 .../org/apache/sis/metadata/sql/util/Syntax.java   |   81 --
 .../apache/sis/metadata/sql/util/TypeMapper.java   |  105 --
 .../apache/sis/metadata/sql/util/package-info.java |   77 --
 .../apache/sis/util/iso/DefaultNameFactory.java    |    2 +-
 .../org/apache/sis/util/iso/DefaultNameSpace.java  |    4 +-
 .../org/apache/sis/util/iso/DefaultRecord.java     |    4 +-
 .../apache/sis/util/iso/DefaultRecordSchema.java   |    2 +-
 .../org/apache/sis/util/iso/DefaultScopedName.java |    2 +-
 .../org/apache/sis/util/iso/RecordDefinition.java  |    2 +-
 .../main/org/apache/sis/util/iso/TypeNames.java    |    2 +-
 .../main/org/apache/sis/util/iso/Types.java        |    2 +-
 .../main/org/apache/sis/xml/MarshallerPool.java    |    2 +-
 .../main/org/apache/sis/xml/Namespaces.java        |    4 +-
 .../main/org/apache/sis/xml/NilObjectHandler.java  |    2 +-
 .../main/org/apache/sis/xml/OutputFactory.java     |    2 +-
 .../main/org/apache/sis/xml/Pooled.java            |    6 +-
 .../main/org/apache/sis/xml/PooledMarshaller.java  |    2 +-
 .../org/apache/sis/xml/PooledUnmarshaller.java     |    2 +-
 .../main/org/apache/sis/xml/ReferenceResolver.java |    8 +-
 .../main/org/apache/sis/xml/TransformVersion.java  |    2 +-
 .../org/apache/sis/xml/TransformingReader.java     |   14 +-
 .../org/apache/sis/xml/TransformingWriter.java     |    4 +-
 .../main/org/apache/sis/xml/ValueConverter.java    |    2 +-
 .../main/org/apache/sis/xml/XML.java               |    2 +-
 .../main/org/apache/sis/xml/XPointer.java          |    2 +-
 .../main/org/apache/sis/xml/bind/Context.java      |    6 +-
 .../apache/sis/xml/bind/IdentifierMapAdapter.java  |    4 +-
 .../sis/xml/bind/NonMarshalledAuthority.java       |    6 +-
 .../apache/sis/xml/bind/SpecializedIdentifier.java |    2 +-
 .../org/apache/sis/xml/bind/cat/CodeListUID.java   |    4 +-
 .../sis/xml/bind/gco/CharSequenceAdapter.java      |    2 +-
 .../sis/xml/bind/gco/GO_CharacterString.java       |    2 +-
 .../org/apache/sis/xml/bind/gco/GO_DateTime.java   |    2 +-
 .../org/apache/sis/xml/bind/gco/GO_Temporal.java   |    2 +-
 .../org/apache/sis/xml/bind/gco/PropertyType.java  |    2 +-
 .../org/apache/sis/xml/bind/gmi/MI_Metadata.java   |    2 +-
 .../org/apache/sis/xml/bind/gmi/package-info.java  |    2 +-
 .../org/apache/sis/xml/bind/gml/DateAdapter.java   |    2 +-
 .../main/org/apache/sis/xml/bind/gml/Measure.java  |    4 +-
 .../org/apache/sis/xml/bind/gml/TM_Primitive.java  |    4 +-
 .../org/apache/sis/xml/bind/gml/TimeInstant.java   |    4 +-
 .../org/apache/sis/xml/bind/gml/TimePeriod.java    |    4 +-
 .../sis/xml/bind/gml/UniversalTimeAdapter.java     |    4 +-
 .../apache/sis/xml/bind/gts/TM_PeriodDuration.java |    4 +-
 .../org/apache/sis/xml/bind/gts/package-info.java  |    2 +-
 .../main/org/apache/sis/xml/bind/lan/Country.java  |    4 +-
 .../apache/sis/xml/bind/lan/LocaleAndCharset.java  |    6 +-
 .../org/apache/sis/xml/bind/lan/PT_Locale.java     |    4 +-
 .../xml/bind/metadata/code/MD_MediumNameCode.java  |    2 +-
 .../sis/xml/bind/metadata/code/package-info.java   |    2 +-
 .../sis/xml/bind/metadata/replace/Parameter.java   |    2 +-
 .../bind/metadata/replace/QualityParameter.java    |    2 +-
 .../xml/bind/metadata/replace/RS_Identifier.java   |    2 +-
 .../bind/metadata/replace/ServiceParameter.java    |    8 +-
 .../xml/bind/metadata/replace/package-info.java    |    2 +-
 .../apache/sis/xml/privy/ExceptionSimplifier.java  |  150 +++
 .../apache/sis/xml/privy/ExternalLinkHandler.java  |  317 ++++++
 .../org/apache/sis/xml/privy/LegacyNamespaces.java |  121 +++
 .../apache/sis/xml/privy/StreamWriterDelegate.java |  243 +++++
 .../main/org/apache/sis/xml/privy/URISource.java   |  125 +++
 .../org/apache/sis/xml/privy/XmlUtilities.java     |  374 +++++++
 .../org/apache/sis/xml/privy/package-info.java     |   34 +
 .../apache/sis/xml/util/ExceptionSimplifier.java   |  150 ---
 .../apache/sis/xml/util/ExternalLinkHandler.java   |  317 ------
 .../org/apache/sis/xml/util/LegacyNamespaces.java  |  121 ---
 .../apache/sis/xml/util/StreamWriterDelegate.java  |  243 -----
 .../main/org/apache/sis/xml/util/URISource.java    |  125 ---
 .../main/org/apache/sis/xml/util/XmlUtilities.java |  374 -------
 .../main/org/apache/sis/xml/util/package-info.java |   34 -
 .../test/org/apache/sis/metadata/Assertions.java   |    2 +-
 .../apache/sis/metadata/MetadataCopierTest.java    |    2 +-
 .../sis/metadata/PropertyConsistencyCheck.java     |    2 +-
 .../sis/metadata/internal/AxisNamesTest.java       |   63 --
 .../sis/metadata/internal/IdentifiersTest.java     |  121 ---
 .../internal/ImplementationHelperTest.java         |   85 --
 .../apache/sis/metadata/internal/MergerTest.java   |  131 ---
 .../sis/metadata/internal/NameMeaningTest.java     |   80 --
 .../sis/metadata/iso/citation/CitationsTest.java   |    4 +-
 .../metadata/iso/citation/DefaultCitationTest.java |    2 +-
 .../iso/citation/DefaultResponsibilityTest.java    |    2 +-
 .../metadata/iso/citation/HardCodedCitations.java  |    4 +-
 .../constraint/DefaultLegalConstraintsTest.java    |    4 +-
 .../sis/metadata/iso/content/DefaultBandTest.java  |    2 +-
 .../sis/metadata/iso/extent/ExtentsTest.java       |    2 +-
 .../iso/identification/DefaultResolutionTest.java  |    2 +-
 .../metadata/iso/lineage/DefaultLineageTest.java   |    2 +-
 .../iso/quality/DefaultQuantitativeResultTest.java |    2 +-
 .../{internal => privy}/AxisDirectionsTest.md      |    0
 .../apache/sis/metadata/privy/AxisNamesTest.java   |   63 ++
 .../apache/sis/metadata/privy/IdentifiersTest.java |  121 +++
 .../metadata/privy/ImplementationHelperTest.java   |   85 ++
 .../org/apache/sis/metadata/privy/MergerTest.java  |  131 +++
 .../apache/sis/metadata/privy/NameMeaningTest.java |   80 ++
 .../sis/metadata/sql/IdentifierGeneratorTest.java  |    2 +-
 .../sis/metadata/sql/MetadataFallbackVerifier.java |    4 +-
 .../org/apache/sis/metadata/sql/TestDatabase.java  |    6 +-
 .../sis/metadata/sql/privy/SQLUtilitiesTest.java   |   58 ++
 .../sis/metadata/sql/privy/ScriptRunnerTest.java   |   89 ++
 .../sis/metadata/sql/privy/TypeMapperTest.java     |   48 +
 .../sis/metadata/sql/util/SQLUtilitiesTest.java    |   58 --
 .../sis/metadata/sql/util/ScriptRunnerTest.java    |   89 --
 .../sis/metadata/sql/util/TypeMapperTest.java      |   48 -
 .../apache/sis/test/mock/FeatureAttributeMock.java |    2 +-
 .../apache/sis/test/mock/IdentifiedObjectMock.java |    4 +-
 .../org/apache/sis/test/mock/MetadataMock.java     |    2 +-
 .../org/apache/sis/test/mock/package-info.java     |    2 +-
 .../apache/sis/util/iso/DefaultLocalNameTest.java  |    2 +-
 .../apache/sis/util/iso/NameMarshallingTest.java   |    2 +-
 .../org/apache/sis/util/iso/TypeNamesTest.java     |    2 +-
 .../sis/xml/CharSequenceSubstitutionTest.java      |    4 +-
 .../test/org/apache/sis/xml/NamespacesTest.java    |    2 +-
 .../test/org/apache/sis/xml/TransformerTest.java   |    2 +-
 .../apache/sis/xml/TransformingNamespacesTest.java |    2 +-
 .../sis/xml/bind/cat/CodeListMarshallingTest.java  |    2 +-
 .../apache/sis/xml/bind/gml/TimePeriodTest.java    |    2 +-
 .../sis/xml/bind/lan/FreeTextMarshallingTest.java  |    2 +-
 .../apache/sis/xml/bind/lan/LanguageCodeTest.java  |    4 +-
 .../sis/xml/privy/DocumentComparatorTest.java      |  137 +++
 .../org/apache/sis/xml/privy/XmlUtilitiesTest.java |  120 +++
 .../sis/xml/test/AnnotationConsistencyCheck.java   |    2 +-
 .../apache/sis/xml/test/DocumentComparator.java    |    4 +-
 .../test/org/apache/sis/xml/test/TestCase.java     |    2 +-
 .../sis/xml/util/DocumentComparatorTest.java       |  137 ---
 .../org/apache/sis/xml/util/XmlUtilitiesTest.java  |  120 ---
 .../org/apache/sis/openoffice/Transformer.java     |    2 +-
 .../sis/openoffice/ReferencingFunctionsTest.java   |    4 +-
 .../org/apache/sis/map/coverage/RenderingData.java |   10 +-
 .../sis/map/coverage/RenderingWorkaround.java      |    2 +-
 .../main/org/apache/sis/portrayal/Canvas.java      |    8 +-
 .../org/apache/sis/portrayal/CanvasContext.java    |    2 +-
 .../org/apache/sis/portrayal/CanvasExtent.java     |    4 +-
 .../org/apache/sis/portrayal/CanvasFollower.java   |    2 +-
 .../org/apache/sis/portrayal/PlanarCanvas.java     |    2 +-
 .../org/apache/sis/style/se1/StyleFactory.java     |    2 +-
 .../referencing/gazetteer/FinalLocationType.java   |    2 +-
 .../referencing/gazetteer/GazetteerFactory.java    |    2 +-
 .../gazetteer/GeohashReferenceSystem.java          |    2 +-
 .../sis/referencing/gazetteer/LocationFormat.java  |    2 +-
 .../gazetteer/MilitaryGridReferenceSystem.java     |    6 +-
 .../gazetteer/ModifiableLocationTypeAdapter.java   |    2 +-
 .../gazetteer/ReferencingByIdentifiers.java        |    4 +-
 .../sis/referencing/gazetteer/LocationViewer.java  |    2 +-
 .../gazetteer/MilitaryGridReferenceSystemTest.java |    2 +-
 ....apache.sis.metadata.privy.ReferencingServices} |    0
 ... org.apache.sis.metadata.sql.privy.Initializer} |    0
 .../main/module-info.java                          |   15 +-
 .../sis/geometry/AbstractDirectPosition.java       |    6 +-
 .../org/apache/sis/geometry/AbstractEnvelope.java  |    8 +-
 .../org/apache/sis/geometry/ArrayEnvelope.java     |    2 +-
 .../org/apache/sis/geometry/CoordinateFormat.java  |   10 +-
 .../main/org/apache/sis/geometry/Envelope2D.java   |    2 +-
 .../org/apache/sis/geometry/EnvelopeReducer.java   |    2 +-
 .../main/org/apache/sis/geometry/Envelopes.java    |   10 +-
 .../org/apache/sis/geometry/GeneralEnvelope.java   |    4 +-
 .../main/org/apache/sis/geometry/Shapes2D.java     |    6 +-
 .../apache/sis/geometry/WraparoundAdjustment.java  |    6 +-
 .../main/org/apache/sis/io/wkt/AbstractParser.java |    2 +-
 .../main/org/apache/sis/io/wkt/Colors.java         |    2 +-
 .../main/org/apache/sis/io/wkt/Element.java        |    4 +-
 .../org/apache/sis/io/wkt/FormattableObject.java   |    4 +-
 .../main/org/apache/sis/io/wkt/Formatter.java      |   12 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |   22 +-
 .../org/apache/sis/io/wkt/MathTransformParser.java |    8 +-
 .../main/org/apache/sis/io/wkt/StoredTree.java     |    2 +-
 .../main/org/apache/sis/io/wkt/Transliterator.java |    6 +-
 .../main/org/apache/sis/io/wkt/VerticalInfo.java   |    2 +-
 .../main/org/apache/sis/io/wkt/WKTDictionary.java  |   10 +-
 .../main/org/apache/sis/io/wkt/WKTFormat.java      |    6 +-
 .../sis/parameter/AbstractParameterDescriptor.java |    4 +-
 .../sis/parameter/DefaultParameterDescriptor.java  |    2 +-
 .../parameter/DefaultParameterDescriptorGroup.java |    4 +-
 .../sis/parameter/DefaultParameterValue.java       |   10 +-
 .../sis/parameter/DefaultParameterValueGroup.java  |    2 +-
 .../apache/sis/parameter/FilteredParameters.java   |    2 +-
 .../sis/parameter/MapProjectionDescriptor.java     |    2 +-
 .../sis/parameter/MapProjectionParameters.java     |    4 +-
 .../org/apache/sis/parameter/MatrixParameters.java |    2 +-
 .../sis/parameter/MatrixParametersAlphaNum.java    |    4 +-
 .../org/apache/sis/parameter/ParameterFormat.java  |    6 +-
 .../apache/sis/parameter/ParameterTableRow.java    |    8 +-
 .../org/apache/sis/parameter/TensorParameters.java |    2 +-
 .../org/apache/sis/parameter/TensorValues.java     |   10 +-
 .../sis/parameter/UninitializedParameter.java      |    2 +-
 .../sis/parameter/UnmodifiableParameterValue.java  |    2 +-
 .../parameter/UnmodifiableParameterValueGroup.java |    2 +-
 .../main/org/apache/sis/parameter/Verifier.java    |    2 +-
 .../sis/referencing/AbstractIdentifiedObject.java  |   20 +-
 .../apache/sis/referencing/AuthorityFactories.java |    2 +-
 .../main/org/apache/sis/referencing/CRS.java       |   12 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |    6 +-
 .../sis/referencing/DefaultObjectDomain.java       |    2 +-
 .../sis/referencing/EPSGFactoryFallback.java       |    6 +-
 .../referencing/EllipsoidalHeightSeparator.java    |   10 +-
 .../sis/referencing/GeodesicsOnEllipsoid.java      |    2 +-
 .../apache/sis/referencing/GeodeticCalculator.java |   14 +-
 .../apache/sis/referencing/IdentifiedObjects.java  |   14 +-
 .../sis/referencing/ImmutableIdentifier.java       |    8 +-
 .../org/apache/sis/referencing/NameIterator.java   |    8 +-
 .../apache/sis/referencing/NamedIdentifier.java    |    2 +-
 .../org/apache/sis/referencing/Properties.java     |    4 +-
 .../sis/referencing/StandardDefinitions.java       |    8 +-
 .../apache/sis/referencing/crs/AbstractCRS.java    |    8 +-
 .../sis/referencing/crs/AbstractDerivedCRS.java    |    6 +-
 .../sis/referencing/crs/DefaultCompoundCRS.java    |    8 +-
 .../sis/referencing/crs/DefaultDerivedCRS.java     |    6 +-
 .../sis/referencing/crs/DefaultEngineeringCRS.java |    4 +-
 .../sis/referencing/crs/DefaultGeodeticCRS.java    |   10 +-
 .../sis/referencing/crs/DefaultGeographicCRS.java  |   10 +-
 .../sis/referencing/crs/DefaultImageCRS.java       |    4 +-
 .../sis/referencing/crs/DefaultParametricCRS.java  |    4 +-
 .../sis/referencing/crs/DefaultProjectedCRS.java   |    8 +-
 .../sis/referencing/crs/DefaultTemporalCRS.java    |    8 +-
 .../sis/referencing/crs/DefaultVerticalCRS.java    |    4 +-
 .../sis/referencing/crs/ExplicitParameters.java    |    6 +-
 .../org/apache/sis/referencing/cs/AbstractCS.java  |   12 +-
 .../apache/sis/referencing/cs/AxesConvention.java  |    2 +-
 .../main/org/apache/sis/referencing/cs/Codes.java  |    4 +-
 .../sis/referencing/cs/CoordinateSystems.java      |    6 +-
 .../apache/sis/referencing/cs/DefaultAffineCS.java |    2 +-
 .../sis/referencing/cs/DefaultCompoundCS.java      |    4 +-
 .../cs/DefaultCoordinateSystemAxis.java            |   12 +-
 .../sis/referencing/cs/DefaultCylindricalCS.java   |    2 +-
 .../sis/referencing/cs/DefaultEllipsoidalCS.java   |    2 +-
 .../apache/sis/referencing/cs/DefaultLinearCS.java |    2 +-
 .../apache/sis/referencing/cs/DefaultPolarCS.java  |    2 +-
 .../sis/referencing/cs/DefaultSphericalCS.java     |    2 +-
 .../apache/sis/referencing/cs/DefaultTimeCS.java   |    2 +-
 .../sis/referencing/cs/DefaultVerticalCS.java      |    2 +-
 .../sis/referencing/cs/DirectionAlongMeridian.java |    6 +-
 .../org/apache/sis/referencing/cs/Normalizer.java  |    6 +-
 .../org/apache/sis/referencing/cs/SubTypes.java    |    2 +-
 .../sis/referencing/datum/AbstractDatum.java       |    8 +-
 .../sis/referencing/datum/BursaWolfParameters.java |    4 +-
 .../sis/referencing/datum/DatumShiftGrid.java      |    4 +-
 .../sis/referencing/datum/DefaultEllipsoid.java    |   12 +-
 .../referencing/datum/DefaultEngineeringDatum.java |    2 +-
 .../referencing/datum/DefaultGeodeticDatum.java    |   14 +-
 .../sis/referencing/datum/DefaultImageDatum.java   |    4 +-
 .../referencing/datum/DefaultParametricDatum.java  |    2 +-
 .../referencing/datum/DefaultPrimeMeridian.java    |   14 +-
 .../referencing/datum/DefaultTemporalDatum.java    |    4 +-
 .../referencing/datum/DefaultVerticalDatum.java    |    6 +-
 .../sis/referencing/datum/TimeDependentBWP.java    |    4 +-
 .../apache/sis/referencing/datum/package-info.java |    2 +-
 .../factory/AuthorityFactoryIdentifier.java        |   10 +-
 .../referencing/factory/AuthorityFactoryProxy.java |    2 +-
 .../referencing/factory/CommonAuthorityCode.java   |    4 +-
 .../factory/CommonAuthorityFactory.java            |    4 +-
 .../factory/ConcurrentAuthorityFactory.java        |    4 +-
 .../sis/referencing/factory/FilteredCodes.java     |    2 +-
 .../factory/GeodeticAuthorityFactory.java          |    4 +-
 .../referencing/factory/GeodeticObjectFactory.java |    2 +-
 .../factory/IdentifiedObjectFinder.java            |    2 +-
 .../factory/LazySynchronizedIterator.java          |    2 +-
 .../factory/MultiAuthoritiesFactory.java           |   14 +-
 .../sis/referencing/factory/ReferenceKeeper.java   |    2 +-
 .../referencing/factory/sql/AuthorityCodes.java    |    2 +-
 .../sis/referencing/factory/sql/BursaWolfInfo.java |    2 +-
 .../referencing/factory/sql/EPSGCodeFinder.java    |   10 +-
 .../referencing/factory/sql/EPSGDataAccess.java    |   26 +-
 .../sis/referencing/factory/sql/EPSGFactory.java   |    6 +-
 .../sis/referencing/factory/sql/EPSGInstaller.java |    8 +-
 .../factory/sql/InstallationScriptProvider.java    |    4 +-
 .../sis/referencing/factory/sql/SQLTranslator.java |    8 +-
 .../sis/referencing/factory/sql/TableInfo.java     |    2 +-
 .../sis/referencing/internal/AnnotatedMatrix.java  |    4 +-
 .../sis/referencing/internal/Arithmetic.java       |    6 +-
 .../apache/sis/referencing/internal/Bezier.java    |  532 ++++++++++
 .../sis/referencing/internal/DatabaseListener.java |    4 +-
 .../referencing/internal/EPSGParameterDomain.java  |    2 +-
 .../internal/ImmutableAffineTransform.java         |  314 ++++++
 .../apache/sis/referencing/internal/Legacy.java    |    2 +-
 .../referencing/internal/LinearTransform2D.java    |   39 +
 .../sis/referencing/internal/MergedProperties.java |    2 +-
 .../referencing/internal/ParameterizedAffine.java  |  172 ++++
 .../referencing/internal/ServicesForMetadata.java  |   12 +-
 .../operation/AbstractCoordinateOperation.java     |   20 +-
 .../operation/AbstractSingleOperation.java         |    8 +-
 .../apache/sis/referencing/operation/CRSPair.java  |    4 +-
 .../operation/CoordinateOperationContext.java      |    4 +-
 .../operation/CoordinateOperationFinder.java       |   12 +-
 .../operation/CoordinateOperationRegistry.java     |   12 +-
 .../operation/DefaultConcatenatedOperation.java    |    8 +-
 .../referencing/operation/DefaultConversion.java   |    2 +-
 .../DefaultCoordinateOperationFactory.java         |   10 +-
 .../sis/referencing/operation/DefaultFormula.java  |    2 +-
 .../operation/DefaultOperationMethod.java          |   10 +-
 .../operation/DefaultPassThroughOperation.java     |    4 +-
 .../operation/InverseOperationMethod.java          |    2 +-
 .../operation/MathTransformContext.java            |    2 +-
 .../operation/builder/LinearTransformBuilder.java  |   10 +-
 .../operation/builder/LocalizationGridBuilder.java |    4 +-
 .../operation/builder/ResidualGrid.java            |    6 +-
 .../operation/gridded/CompressedGrid.java          |    2 +-
 .../referencing/operation/gridded/GridGroup.java   |    8 +-
 .../referencing/operation/gridded/LoadedGrid.java  |    2 +-
 .../sis/referencing/operation/gridded/Tile.java    |  679 +++++++++++++
 .../operation/gridded/TileOrganizer.java           |  567 +++++++++++
 .../operation/matrix/AffineTransforms2D.java       |    4 +-
 .../operation/matrix/GeneralMatrix.java            |    4 +-
 .../sis/referencing/operation/matrix/Matrices.java |    8 +-
 .../sis/referencing/operation/matrix/Matrix1.java  |    2 +-
 .../sis/referencing/operation/matrix/Matrix2.java  |    2 +-
 .../referencing/operation/matrix/MatrixSIS.java    |    4 +-
 .../sis/referencing/operation/matrix/Solver.java   |    2 +-
 .../operation/matrix/UnmodifiableMatrix.java       |    2 +-
 .../operation/projection/AlbersEqualArea.java      |    4 +-
 .../operation/projection/AuthalicConversion.java   |    2 +-
 .../operation/projection/ConformalProjection.java  |    2 +-
 .../operation/projection/CylindricalEqualArea.java |    2 +-
 .../operation/projection/Initializer.java          |    6 +-
 .../projection/LambertAzimuthalEqualArea.java      |    4 +-
 .../projection/LambertConicConformal.java          |    6 +-
 .../operation/projection/LongitudeWraparound.java  |    4 +-
 .../referencing/operation/projection/Mercator.java |    2 +-
 .../operation/projection/MeridianArcBased.java     |    2 +-
 .../operation/projection/NormalizedProjection.java |    8 +-
 .../operation/projection/ObliqueMercator.java      |    2 +-
 .../operation/projection/ObliqueStereographic.java |    4 +-
 .../operation/projection/PolarStereographic.java   |    6 +-
 .../operation/projection/SatelliteTracking.java    |    2 +-
 .../operation/projection/TransverseMercator.java   |    2 +-
 .../operation/projection/ZonedGridSystem.java      |    2 +-
 .../operation/provider/AbstractProvider.java       |    2 +-
 .../sis/referencing/operation/provider/Affine.java |    4 +-
 .../referencing/operation/provider/EPSGName.java   |    2 +-
 .../operation/provider/Equirectangular.java        |    6 +-
 .../provider/FranceGeocentricInterpolation.java    |    4 +-
 .../operation/provider/GeocentricAffine.java       |    6 +-
 .../GeocentricAffineBetweenGeographic.java         |    2 +-
 .../provider/GeocentricToTopocentric.java          |    2 +-
 .../operation/provider/Geographic2Dto3D.java       |    2 +-
 .../operation/provider/Geographic3Dto2D.java       |    4 +-
 .../operation/provider/GeographicOffsets2D.java    |    2 +-
 .../operation/provider/GeographicRedimension.java  |    2 +-
 .../operation/provider/GeographicToGeocentric.java |    2 +-
 .../operation/provider/Interpolation1D.java        |    2 +-
 .../operation/provider/LambertConformal2SP.java    |    2 +-
 .../operation/provider/LongitudeRotation.java      |    2 +-
 .../operation/provider/MapProjection.java          |    2 +-
 .../operation/provider/Mercator1SP.java            |    2 +-
 .../operation/provider/MillerCylindrical.java      |    2 +-
 .../referencing/operation/provider/Molodensky.java |    6 +-
 .../sis/referencing/operation/provider/NTv2.java   |    4 +-
 .../operation/provider/PolarStereographicA.java    |    6 +-
 .../operation/provider/SatelliteTracking.java      |    2 +-
 .../referencing/operation/provider/Sinusoidal.java |    2 +-
 .../operation/provider/TransverseMercator.java     |    6 +-
 .../transform/AbstractLinearTransform.java         |    4 +-
 .../operation/transform/AbstractMathTransform.java |    6 +-
 .../transform/AbstractMathTransform2D.java         |    2 +-
 .../operation/transform/CartesianToPolar.java      |    2 +-
 .../operation/transform/ConcatenatedTransform.java |    6 +-
 .../operation/transform/ContextualParameters.java  |   14 +-
 .../transform/CoordinateSystemTransform.java       |    8 +-
 .../transform/DefaultMathTransformFactory.java     |   14 +-
 .../transform/EllipsoidToCentricTransform.java     |   12 +-
 .../transform/ExponentialTransform1D.java          |    2 +-
 .../operation/transform/IdentityTransform.java     |    2 +-
 .../transform/InterpolatedGeocentricTransform.java |    2 +-
 .../operation/transform/InterpolatedTransform.java |    4 +-
 .../operation/transform/LinearInterpolator1D.java  |    2 +-
 .../operation/transform/LinearTransform1D.java     |    6 +-
 .../transform/LogarithmicTransform1D.java          |    2 +-
 .../operation/transform/MathTransforms.java        |    6 +-
 .../operation/transform/MolodenskyTransform.java   |    2 +-
 .../operation/transform/OperationMethodSet.java    |    2 +-
 .../operation/transform/PassThroughTransform.java  |    6 +-
 .../operation/transform/PolarToCartesian.java      |    2 +-
 .../operation/transform/PoleRotation.java          |    6 +-
 .../operation/transform/PowerTransform1D.java      |    2 +-
 .../operation/transform/ProjectiveTransform.java   |    8 +-
 .../operation/transform/ProjectiveTransform2D.java |    4 +-
 .../operation/transform/ScaleTransform.java        |    4 +-
 .../transform/SpecializableTransform.java          |    2 +-
 .../operation/transform/TransformSeparator.java    |    4 +-
 .../operation/transform/TranslationTransform.java  |    4 +-
 .../operation/transform/WraparoundTransform.java   |    2 +-
 .../org/apache/sis/referencing/package-info.java   |    2 +-
 .../sis/referencing/privy/AbstractShape.java       |   79 ++
 .../apache/sis/referencing/privy/AffineMatrix.java |  242 +++++
 .../sis/referencing/privy/AffineTransform2D.java   |  491 ++++++++++
 .../apache/sis/referencing/privy/AxesMapper.java   |  156 +++
 .../sis/referencing/privy/AxisDirections.java      |  831 ++++++++++++++++
 .../referencing/privy/CoordinateOperations.java    |  361 +++++++
 .../sis/referencing/privy/DefinitionVerifier.java  |  359 +++++++
 .../sis/referencing/privy/DirectPositionView.java  |  153 +++
 .../privy/EllipsoidalHeightCombiner.java           |  227 +++++
 .../referencing/privy/ExtendedPrecisionMatrix.java |  150 +++
 .../sis/referencing/privy/ExtentSelector.java      |  558 +++++++++++
 .../org/apache/sis/referencing/privy/Formulas.java |  292 ++++++
 .../referencing/privy/GeodeticObjectBuilder.java   |  674 +++++++++++++
 .../sis/referencing/privy/IntervalRectangle.java   |  573 +++++++++++
 .../org/apache/sis/referencing/privy/LazySet.java  |  247 +++++
 .../referencing/privy/NilReferencingObject.java    |   99 ++
 .../privy/PositionalAccuracyConstant.java          |  246 +++++
 .../privy/ReferencingFactoryContainer.java         |  403 ++++++++
 .../referencing/privy/ReferencingUtilities.java    |  687 +++++++++++++
 .../sis/referencing/privy/ShapeUtilities.java      |  426 ++++++++
 .../sis/referencing/privy/TemporalAccessor.java    |  138 +++
 .../apache/sis/referencing/privy/WKTKeywords.java  |  299 ++++++
 .../apache/sis/referencing/privy/WKTUtilities.java |  548 +++++++++++
 .../referencing/privy/WraparoundApplicator.java    |  222 +++++
 .../referencing/privy/WraparoundAxesFinder.java    |   83 ++
 .../apache/sis/referencing/privy/package-info.java |   30 +
 .../apache/sis/referencing/util/AxesMapper.java    |  156 ---
 .../sis/referencing/util/AxisDirections.java       |  831 ----------------
 .../sis/referencing/util/CoordinateOperations.java |  361 -------
 .../sis/referencing/util/DefinitionVerifier.java   |  359 -------
 .../sis/referencing/util/DirectPositionView.java   |  153 ---
 .../util/EllipsoidalHeightCombiner.java            |  227 -----
 .../referencing/util/ExtendedPrecisionMatrix.java  |  150 ---
 .../sis/referencing/util/ExtentSelector.java       |  558 -----------
 .../org/apache/sis/referencing/util/Formulas.java  |  292 ------
 .../referencing/util/GeodeticObjectBuilder.java    |  674 -------------
 .../org/apache/sis/referencing/util/LazySet.java   |  247 -----
 .../sis/referencing/util/NilReferencingObject.java |   99 --
 .../util/PositionalAccuracyConstant.java           |  246 -----
 .../util/ReferencingFactoryContainer.java          |  403 --------
 .../sis/referencing/util/ReferencingUtilities.java |  687 -------------
 .../sis/referencing/util/TemporalAccessor.java     |  138 ---
 .../apache/sis/referencing/util/WKTKeywords.java   |  299 ------
 .../apache/sis/referencing/util/WKTUtilities.java  |  548 -----------
 .../sis/referencing/util/WraparoundApplicator.java |  222 -----
 .../sis/referencing/util/WraparoundAxesFinder.java |   83 --
 .../sis/referencing/util/j2d/AbstractShape.java    |   79 --
 .../sis/referencing/util/j2d/AffineMatrix.java     |  243 -----
 .../referencing/util/j2d/AffineTransform2D.java    |  490 ----------
 .../apache/sis/referencing/util/j2d/Bezier.java    |  532 ----------
 .../util/j2d/ImmutableAffineTransform.java         |  314 ------
 .../referencing/util/j2d/IntervalRectangle.java    |  573 -----------
 .../referencing/util/j2d/LinearTransform2D.java    |   39 -
 .../referencing/util/j2d/ParameterizedAffine.java  |  171 ----
 .../sis/referencing/util/j2d/ShapeUtilities.java   |  426 --------
 .../org/apache/sis/referencing/util/j2d/Tile.java  |  678 -------------
 .../sis/referencing/util/j2d/TileOrganizer.java    |  565 -----------
 .../sis/referencing/util/j2d/package-info.java     |   30 -
 .../apache/sis/referencing/util/package-info.java  |   30 -
 .../bind/referencing/CC_CoordinateOperation.java   |    2 +-
 .../referencing/CC_GeneralOperationParameter.java  |    2 +-
 .../sis/xml/bind/referencing/CS_AxisDirection.java |    2 +-
 .../sis/xml/bind/referencing/CS_RangeMeaning.java  |    2 +-
 .../org/apache/sis/xml/bind/referencing/Code.java  |    8 +-
 .../org/apache/sis/geometry/EnvelopesTest.java     |    2 +-
 .../apache/sis/geometry/GeneralEnvelopeTest.java   |    2 +-
 .../org/apache/sis/geometry/TransformTestCase.java |    4 +-
 .../sis/geometry/WraparoundAdjustmentTest.java     |    2 +-
 .../sis/io/wkt/GeodeticObjectParserTest.java       |    6 +-
 .../org/apache/sis/io/wkt/TransliteratorTest.java  |    2 +-
 .../DefaultParameterDescriptorGroupTest.java       |    2 +-
 .../parameter/DefaultParameterDescriptorTest.java  |    2 +-
 .../sis/parameter/DefaultParameterValueTest.java   |    2 +-
 .../sis/parameter/MapProjectionParametersTest.java |   18 +-
 .../apache/sis/parameter/TensorParametersTest.java |    4 +-
 .../org/apache/sis/parameter/TensorValuesTest.java |    6 +-
 .../org/apache/sis/referencing/Assertions.java     |    2 +-
 .../sis/referencing/AuthorityFactoriesTest.java    |    2 +-
 .../org/apache/sis/referencing/CommonCRSTest.java  |    4 +-
 .../sis/referencing/GeodesicsOnEllipsoidTest.java  |    6 +-
 .../sis/referencing/GeodeticCalculatorTest.java    |    6 +-
 .../sis/referencing/GeodeticObjectVerifier.java    |    2 +-
 .../sis/referencing/ImmutableIdentifierTest.java   |    4 +-
 .../sis/referencing/StandardDefinitionsTest.java   |    2 +-
 .../sis/referencing/crs/DefaultDerivedCRSTest.java |    2 +-
 .../referencing/crs/DefaultProjectedCRSTest.java   |    4 +-
 .../referencing/crs/DefaultTemporalCRSTest.java    |    4 +-
 .../referencing/cs/DefaultCylindricalCSTest.java   |    2 +-
 .../sis/referencing/cs/DefaultPolarCSTest.java     |    2 +-
 .../sis/referencing/cs/DefaultSphericalCSTest.java |    2 +-
 .../apache/sis/referencing/cs/HardCodedAxes.java   |    4 +-
 .../datum/DefaultGeodeticDatumTest.java            |    2 +-
 .../datum/DefaultPrimeMeridianTest.java            |    2 +-
 .../datum/DefaultTemporalDatumTest.java            |    2 +-
 .../datum/DefaultVerticalDatumTest.java            |    2 +-
 .../sis/referencing/datum/GeodeticDatumMock.java   |    2 +-
 .../sis/referencing/datum/HardCodedDatum.java      |    2 +-
 .../referencing/datum/TimeDependentBWPTest.java    |    2 +-
 .../factory/CommonAuthorityFactoryTest.java        |    2 +-
 .../factory/ConcurrentAuthorityFactoryTest.java    |    2 +-
 .../sis/referencing/factory/TestFactorySource.java |    4 +-
 .../referencing/factory/sql/EPSGInstallerTest.java |    4 +-
 .../factory/sql/epsg/DataScriptFormatter.java      |    4 +-
 .../operation/CoordinateOperationFinderTest.java   |    8 +-
 .../operation/CoordinateOperationRegistryTest.java |    2 +-
 .../operation/DefaultConversionTest.java           |    4 +-
 .../DefaultCoordinateOperationFactoryTest.java     |    6 +-
 .../operation/DefaultTransformationTest.java       |    2 +-
 .../operation/matrix/GeneralMatrixTest.java        |    2 +-
 .../referencing/operation/matrix/MatricesTest.java |    2 +-
 .../operation/matrix/MatrixTestCase.java           |    4 +-
 .../operation/projection/AlbersEqualAreaTest.java  |    2 +-
 .../projection/AuthalicConversionTest.java         |    2 +-
 .../projection/AzimuthalEquidistantTest.java       |    2 +-
 .../operation/projection/CassiniSoldnerTest.java   |    2 +-
 .../projection/ConformalProjectionTest.java        |    2 +-
 .../projection/CylindricalEqualAreaTest.java       |    2 +-
 .../operation/projection/EquirectangularTest.java  |    4 +-
 .../operation/projection/InitializerTest.java      |    2 +-
 .../projection/LambertAzimuthalEqualAreaTest.java  |    6 +-
 .../projection/LambertConicConformalTest.java      |    2 +-
 .../projection/MapProjectionTestCase.java          |    4 +-
 .../projection/MercatorMethodComparison.java       |    4 +-
 .../operation/projection/MercatorTest.java         |    4 +-
 .../operation/projection/MollweideTest.java        |    2 +-
 .../sis/referencing/operation/projection/NoOp.java |    2 +-
 .../projection/NormalizedProjectionTest.java       |    2 +-
 .../projection/ObliqueStereographicTest.java       |    2 +-
 .../projection/PolarStereographicTest.java         |    2 +-
 .../operation/projection/PolyconicTest.java        |    2 +-
 .../operation/projection/ProjectionBenchmark.java  |    4 +-
 .../projection/ProjectionResultComparator.java     |    2 +-
 .../projection/SatelliteTrackingTest.java          |    2 +-
 .../operation/projection/SinusoidalTest.java       |    2 +-
 .../projection/TransverseMercatorTest.java         |    2 +-
 .../operation/projection/ZonedGridSystemTest.java  |    4 +-
 .../provider/CoordinateFrameRotationTest.java      |    2 +-
 .../provider/GeocentricTranslationTest.java        |    2 +-
 .../operation/provider/GeographicOffsetsTest.java  |    2 +-
 .../operation/provider/MapProjectionTest.java      |    2 +-
 .../referencing/operation/provider/NTv2Test.java   |    2 +-
 .../provider/PositionVector7ParamTest.java         |    2 +-
 .../operation/provider/TransverseMercatorTest.java |    2 +-
 .../transform/AbridgedMolodenskyTransformTest.java |    2 +-
 .../transform/ConcatenatedTransformTest.java       |    2 +-
 .../operation/transform/CoordinateDomain.java      |    2 +-
 .../transform/DefaultMathTransformFactoryTest.java |    2 +-
 .../operation/transform/DomainDefinitionTest.java  |    2 +-
 .../transform/EllipsoidToCentricTransformTest.java |    2 +-
 .../transform/InterpolatedTransformTest.java       |    2 +-
 .../operation/transform/LinearTransformTest.java   |    2 +-
 .../transform/MolodenskyTransformTest.java         |    2 +-
 .../transform/OperationMethodSetTest.java          |    2 +-
 .../operation/transform/PoleRotationTest.java      |    2 +-
 .../transform/ProjectiveTransformTest.java         |    6 +-
 .../operation/transform/ScaleTransformTest.java    |    2 +-
 .../transform/SpecializableTransformTest.java      |    2 +-
 .../transform/TranslationTransformTest.java        |    4 +-
 .../transform/WraparoundTransformTest.java         |    2 +-
 .../sis/referencing/privy/AbstractShapeTest.java   |   50 +
 .../sis/referencing/privy/AxesMapperTest.java      |  101 ++
 .../sis/referencing/privy/AxisDirectionsTest.java  |  545 +++++++++++
 .../privy/CoordinateOperationsTest.java            |   80 ++
 .../referencing/privy/DefinitionVerifierTest.java  |  103 ++
 .../privy/EllipsoidalHeightCombinerTest.java       |  171 ++++
 .../sis/referencing/privy/ExtentSelectorTest.java  |  210 ++++
 .../apache/sis/referencing/privy/FormulasTest.java |  142 +++
 .../privy/GeodeticObjectBuilderTest.java           |   66 ++
 .../apache/sis/referencing/privy/LazySetTest.java  |  118 +++
 .../privy/PositionalAccuracyConstantTest.java      |   68 ++
 .../privy/ReferencingUtilitiesTest.java            |  128 +++
 .../sis/referencing/privy/ShapeUtilitiesExt.java   |  147 +++
 .../sis/referencing/privy/ShapeUtilitiesTest.java  |  248 +++++
 .../referencing/privy/ShapeUtilitiesViewer.java    |  266 +++++
 .../sis/referencing/privy/WKTKeywordsTest.java     |   99 ++
 .../sis/referencing/privy/WKTUtilitiesTest.java    |  102 ++
 .../privy/WraparoundApplicatorTest.java            |   47 +
 .../report/CoordinateOperationMethods.java         |    4 +-
 .../sis/referencing/report/HTMLGenerator.java      |    2 +-
 .../sis/referencing/util/AxesMapperTest.java       |  101 --
 .../sis/referencing/util/AxisDirectionsTest.java   |  545 -----------
 .../referencing/util/CoordinateOperationsTest.java |   80 --
 .../referencing/util/DefinitionVerifierTest.java   |  103 --
 .../util/EllipsoidalHeightCombinerTest.java        |  171 ----
 .../sis/referencing/util/ExtentSelectorTest.java   |  210 ----
 .../apache/sis/referencing/util/FormulasTest.java  |  142 ---
 .../util/GeodeticObjectBuilderTest.java            |   66 --
 .../apache/sis/referencing/util/LazySetTest.java   |  118 ---
 .../util/PositionalAccuracyConstantTest.java       |   68 --
 .../referencing/util/ReferencingUtilitiesTest.java |  128 ---
 .../sis/referencing/util/WKTKeywordsTest.java      |   99 --
 .../sis/referencing/util/WKTUtilitiesTest.java     |  102 --
 .../referencing/util/WraparoundApplicatorTest.java |   47 -
 .../referencing/util/j2d/AbstractShapeTest.java    |   50 -
 .../referencing/util/j2d/ShapeUtilitiesExt.java    |  144 ---
 .../referencing/util/j2d/ShapeUtilitiesTest.java   |  248 -----
 .../referencing/util/j2d/ShapeUtilitiesViewer.java |  266 -----
 .../sis/test/integration/ConsistencyTest.java      |    2 +-
 .../apache/sis/test/integration/MetadataTest.java  |    4 +-
 .../org/apache/sis/test/widget/package-info.java   |    2 +-
 .../CC_OperationParameterGroupTest.java            |    2 +-
 .../apache/sis/xml/bind/referencing/CodeTest.java  |    2 +-
 .../main/org/apache/sis/storage/landsat/Band.java  |    2 +-
 .../org/apache/sis/storage/landsat/BandGroup.java  |    2 +-
 .../apache/sis/storage/landsat/LandsatStore.java   |    2 +-
 .../apache/sis/storage/landsat/MetadataReader.java |   12 +-
 .../sis/storage/geotiff/CompressedSubset.java      |    2 +-
 .../apache/sis/storage/geotiff/Compression.java    |    2 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |    8 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |    4 +-
 .../sis/storage/geotiff/GeoTiffStoreProvider.java  |    2 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   10 +-
 .../sis/storage/geotiff/MultiResolutionImage.java  |    2 +-
 .../org/apache/sis/storage/geotiff/Writer.java     |    4 +-
 .../sis/storage/geotiff/reader/CRSBuilder.java     |   14 +-
 .../geotiff/reader/GridGeometryBuilder.java        |    2 +-
 .../apache/sis/storage/geotiff/reader/Type.java    |    2 +-
 .../sis/storage/geotiff/reader/XMLMetadata.java    |    4 +-
 .../sis/storage/geotiff/spi/SchemaModifier.java    |    2 +-
 .../sis/storage/geotiff/writer/GeoEncoder.java     |    8 +-
 .../storage/geotiff/writer/ReformattedImage.java   |    2 +-
 .../org/apache/sis/storage/geotiff/WriterTest.java |    2 +-
 .../storage/geotiff/reader/XMLMetadataTest.java    |    2 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |    6 +-
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |    4 +-
 .../org/apache/sis/storage/netcdf/base/Axis.java   |    2 +-
 .../apache/sis/storage/netcdf/base/AxisType.java   |    2 +-
 .../apache/sis/storage/netcdf/base/CRSBuilder.java |    4 +-
 .../apache/sis/storage/netcdf/base/CRSMerger.java  |    4 +-
 .../apache/sis/storage/netcdf/base/Convention.java |    2 +-
 .../apache/sis/storage/netcdf/base/Decoder.java    |    4 +-
 .../apache/sis/storage/netcdf/base/FeatureSet.java |    4 +-
 .../org/apache/sis/storage/netcdf/base/Grid.java   |    2 +-
 .../sis/storage/netcdf/base/GridCacheKey.java      |    2 +-
 .../sis/storage/netcdf/base/GridMapping.java       |    6 +-
 .../org/apache/sis/storage/netcdf/base/HYCOM.java  |    2 +-
 .../apache/sis/storage/netcdf/base/Linearizer.java |    6 +-
 .../sis/storage/netcdf/base/NamedElement.java      |    2 +-
 .../org/apache/sis/storage/netcdf/base/Node.java   |    2 +-
 .../sis/storage/netcdf/base/RasterResource.java    |    8 +-
 .../apache/sis/storage/netcdf/base/Variable.java   |    6 +-
 .../sis/storage/netcdf/classic/ChannelDecoder.java |    6 +-
 .../sis/storage/netcdf/classic/GridInfo.java       |    2 +-
 .../sis/storage/netcdf/classic/VariableInfo.java   |    4 +-
 .../sis/storage/netcdf/ucar/DimensionWrapper.java  |    2 +-
 .../sis/storage/netcdf/ucar/GridWrapper.java       |    2 +-
 .../sis/storage/netcdf/ucar/VariableWrapper.java   |    2 +-
 .../main/org/apache/sis/storage/sql/SQLStore.java  |    2 +-
 .../org/apache/sis/storage/sql/feature/Column.java |    6 +-
 .../apache/sis/storage/sql/feature/Database.java   |   10 +-
 .../sis/storage/sql/feature/FeatureAdapter.java    |    2 +-
 .../sis/storage/sql/feature/FeatureAnalyzer.java   |    2 +-
 .../sis/storage/sql/feature/FeatureIterator.java   |    2 +-
 .../sis/storage/sql/feature/FeatureStream.java     |    6 +-
 .../sis/storage/sql/feature/InfoStatements.java    |    8 +-
 .../apache/sis/storage/sql/feature/PrimaryKey.java |    4 +-
 .../sis/storage/sql/feature/QueryAnalyzer.java     |    4 +-
 .../apache/sis/storage/sql/feature/Relation.java   |    4 +-
 .../sis/storage/sql/feature/SelectionClause.java   |    2 +-
 .../storage/sql/feature/SelectionClauseWriter.java |    4 +-
 .../org/apache/sis/storage/sql/feature/Table.java  |    4 +-
 .../sis/storage/sql/feature/TableAnalyzer.java     |    6 +-
 .../sis/storage/sql/feature/TableReference.java    |    2 +-
 .../sis/storage/sql/feature/ValueGetter.java       |    4 +-
 .../sis/storage/sql/postgis/ExtentEstimator.java   |    2 +-
 .../apache/sis/storage/sql/postgis/Postgres.java   |    2 +-
 .../sis/storage/sql/postgis/RasterReader.java      |    8 +-
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |    2 +-
 .../sql/feature/TemporalValueGetterTest.java       |    2 +-
 .../apache/sis/storage/sql/postgis/TestRaster.java |    2 +-
 .../main/org/apache/sis/storage/gpx/Metadata.java  |    2 +-
 .../main/org/apache/sis/storage/gpx/Reader.java    |    2 +-
 .../main/org/apache/sis/storage/gpx/Types.java     |    2 +-
 .../main/org/apache/sis/storage/gpx/Writer.java    |    2 +-
 .../sis/storage/xml/stream/FormattedWriter.java    |    2 +-
 .../sis/storage/xml/stream/NamespaceEraser.java    |    2 +-
 .../sis/storage/xml/stream/StaxDataStore.java      |    6 +-
 .../sis/storage/xml/stream/StaxStreamReader.java   |    4 +-
 .../test/org/apache/sis/storage/gpx/TypesTest.java |    2 +-
 .../apache/sis/io/stream/ChannelDataOutput.java    |    2 +-
 .../apache/sis/io/stream/FileCacheByteChannel.java |    2 +-
 .../main/org/apache/sis/io/stream/IOUtilities.java |    2 +-
 .../sis/storage/AbstractGridCoverageResource.java  |    2 +-
 .../org/apache/sis/storage/CoverageSubset.java     |    6 +-
 .../main/org/apache/sis/storage/DataStore.java     |    2 +-
 .../org/apache/sis/storage/DataStoreRegistry.java  |    4 +-
 .../main/org/apache/sis/storage/FeatureNaming.java |    2 +-
 .../main/org/apache/sis/storage/FeatureQuery.java  |    8 +-
 .../main/org/apache/sis/storage/FeatureSubset.java |    2 +-
 .../main/org/apache/sis/storage/ProbeResult.java   |    2 +-
 .../org/apache/sis/storage/StorageConnector.java   |    2 +-
 .../aggregate/BandAggregateGridResource.java       |    6 +-
 .../storage/aggregate/ConcatenatedFeatureSet.java  |    4 +-
 .../aggregate/ConcatenatedGridCoverage.java        |    4 +-
 .../aggregate/ConcatenatedGridResource.java        |    6 +-
 .../sis/storage/aggregate/CoverageAggregator.java  |    2 +-
 .../sis/storage/aggregate/DimensionSelector.java   |    2 +-
 .../apache/sis/storage/aggregate/GridSlice.java    |    4 +-
 .../org/apache/sis/storage/aggregate/Group.java    |    2 +-
 .../sis/storage/aggregate/GroupAggregate.java      |    4 +-
 .../sis/storage/aggregate/JoinFeatureSet.java      |    2 +-
 .../sis/storage/aggregate/MergeStrategy.java       |    4 +-
 .../main/org/apache/sis/storage/base/CodeType.java |    4 +-
 .../org/apache/sis/storage/base/LegalSymbols.java  |    2 +-
 .../apache/sis/storage/base/MetadataBuilder.java   |    6 +-
 .../org/apache/sis/storage/base/PRJDataStore.java  |    2 +-
 .../apache/sis/storage/base/ResourceLineage.java   |    4 +-
 .../apache/sis/storage/base/SimpleAggregate.java   |    2 +-
 .../apache/sis/storage/base/StoreUtilities.java    |    2 +-
 .../sis/storage/base/TiledDeferredImage.java       |    2 +-
 .../apache/sis/storage/base/TiledGridCoverage.java |    4 +-
 .../apache/sis/storage/base/TiledGridResource.java |    2 +-
 .../org/apache/sis/storage/base/URIDataStore.java  |    4 +-
 .../sis/storage/csv/MovingFeatureBuilder.java      |    4 +-
 .../main/org/apache/sis/storage/csv/Store.java     |    8 +-
 .../org/apache/sis/storage/csv/TimeEncoding.java   |    2 +-
 .../apache/sis/storage/esri/AsciiGridStore.java    |    2 +-
 .../org/apache/sis/storage/esri/RasterStore.java   |   12 +-
 .../apache/sis/storage/esri/RawRasterReader.java   |    8 +-
 .../apache/sis/storage/esri/RawRasterStore.java    |    6 +-
 .../apache/sis/storage/event/StoreListeners.java   |    2 +-
 .../main/org/apache/sis/storage/folder/Store.java  |    2 +-
 .../sis/storage/image/WorldFileResource.java       |    6 +-
 .../apache/sis/storage/image/WorldFileStore.java   |    4 +-
 .../org/apache/sis/storage/wkt/StoreFormat.java    |    2 +-
 .../org/apache/sis/storage/wkt/StoreProvider.java  |    2 +-
 .../main/org/apache/sis/storage/xml/Store.java     |    4 +-
 .../org/apache/sis/storage/xml/StoreProvider.java  |    2 +-
 .../org/apache/sis/storage/CoverageQueryTest.java  |    2 +-
 .../org/apache/sis/storage/FeatureQueryTest.java   |    2 +-
 .../sis/storage/aggregate/JoinFeatureSetTest.java  |    2 +-
 .../sis/storage/base/MemoryGridResourceTest.java   |    2 +-
 .../sis/storage/test/CoverageReadConsistency.java  |    4 +-
 .../apache/sis/storage/test/SubsampledImage.java   |    2 +-
 .../sis/storage/xml/MimeTypeDetectorTest.java      |    2 +-
 .../test/org/apache/sis/storage/xml/StoreTest.java |    2 +-
 .../src/org.apache.sis.util/main/module-info.java  |    4 +-
 .../org/apache/sis/converter/StringConverter.java  |    4 +-
 .../main/org/apache/sis/io/CompoundFormat.java     |    6 +-
 .../main/org/apache/sis/io/DefaultFormat.java      |    2 +-
 .../org/apache/sis/io/IdentifiedObjectFormat.java  |    6 +-
 .../main/org/apache/sis/io/LineAppender.java       |    2 +-
 .../main/org/apache/sis/io/TableAppender.java      |    2 +-
 .../main/org/apache/sis/math/ArrayVector.java      |    2 +-
 .../main/org/apache/sis/math/DecimalFunctions.java |    2 +-
 .../main/org/apache/sis/math/Fraction.java         |    2 +-
 .../main/org/apache/sis/math/Line.java             |    6 +-
 .../org/apache/sis/math/LinearlyDerivedVector.java |    2 +-
 .../main/org/apache/sis/math/MathFunctions.java    |    6 +-
 .../main/org/apache/sis/math/Plane.java            |    6 +-
 .../main/org/apache/sis/math/Statistics.java       |    4 +-
 .../main/org/apache/sis/math/StatisticsFormat.java |    2 +-
 .../main/org/apache/sis/math/Vector.java           |    2 +-
 .../main/org/apache/sis/measure/AbstractUnit.java  |    2 +-
 .../main/org/apache/sis/measure/Angle.java         |    2 +-
 .../main/org/apache/sis/measure/AngleFormat.java   |    4 +-
 .../org/apache/sis/measure/LinearConverter.java    |    4 +-
 .../main/org/apache/sis/measure/NumberRange.java   |    2 +-
 .../main/org/apache/sis/measure/Prefixes.java      |    2 +-
 .../main/org/apache/sis/measure/Range.java         |    2 +-
 .../main/org/apache/sis/measure/RangeFormat.java   |    6 +-
 .../main/org/apache/sis/measure/Scalar.java        |    2 +-
 .../apache/sis/measure/SexagesimalConverter.java   |    2 +-
 .../main/org/apache/sis/measure/UnitDimension.java |    2 +-
 .../main/org/apache/sis/measure/UnitFormat.java    |    6 +-
 .../main/org/apache/sis/measure/UnitRegistry.java  |    2 +-
 .../main/org/apache/sis/measure/UnitServices.java  |    2 +-
 .../main/org/apache/sis/measure/Units.java         |    2 +-
 .../main/org/apache/sis/setup/About.java           |    6 +-
 .../main/org/apache/sis/setup/Configuration.java   |    2 +-
 .../apache/sis/setup/InstallationResources.java    |    6 +-
 .../apache/sis/setup/OptionalInstallations.java    |    2 +-
 .../main/org/apache/sis/system/DataDirectory.java  |    2 +-
 .../sis/util/AbstractInternationalString.java      |    2 +-
 .../main/org/apache/sis/util/ArgumentChecks.java   |    2 +-
 .../main/org/apache/sis/util/Characters.java       |    4 +-
 .../main/org/apache/sis/util/Numbers.java          |    4 +-
 .../apache/sis/util/collection/CodeListSet.java    |    4 +-
 .../org/apache/sis/util/collection/Containers.java |    2 +-
 .../sis/util/collection/DefaultTreeTable.java      |    4 +-
 .../org/apache/sis/util/collection/DerivedSet.java |    2 +-
 .../apache/sis/util/collection/IntegerList.java    |    2 +-
 .../sis/util/collection/TreeTableFormat.java       |    6 +-
 .../apache/sis/util/internal/AbstractIterator.java |   63 --
 .../org/apache/sis/util/internal/AbstractMap.java  |  709 --------------
 .../apache/sis/util/internal/AbstractMapEntry.java |  100 --
 .../sis/util/internal/AutoMessageFormat.java       |    1 +
 .../main/org/apache/sis/util/internal/Bag.java     |   99 --
 .../apache/sis/util/internal/CheckedArrayList.java |  324 -------
 .../apache/sis/util/internal/CheckedHashSet.java   |  115 ---
 .../org/apache/sis/util/internal/CloneAccess.java  |   46 -
 .../main/org/apache/sis/util/internal/Cloner.java  |  267 -----
 .../org/apache/sis/util/internal/CodeLists.java    |  169 ----
 .../apache/sis/util/internal/CollectionsExt.java   |  880 -----------------
 .../org/apache/sis/util/internal/Constants.java    |  266 -----
 .../apache/sis/util/internal/DefinitionURI.java    |  749 --------------
 .../org/apache/sis/util/internal/DoubleDouble.java |  918 ------------------
 .../org/apache/sis/util/internal/EmptyQueue.java   |   68 --
 .../sis/util/internal/ListOfUnknownSize.java       |  259 -----
 .../sis/util/internal/LocalizedParseException.java |  195 ----
 .../apache/sis/util/internal/MetadataServices.java |  186 ----
 .../org/apache/sis/util/internal/Numerics.java     |  648 -------------
 .../apache/sis/util/internal/PropertyFormat.java   |  238 -----
 .../apache/sis/util/internal/SetOfUnknownSize.java |  199 ----
 .../sis/util/internal/StandardDateFormat.java      |  513 ----------
 .../main/org/apache/sis/util/internal/Strings.java |  420 --------
 .../sis/util/internal/TreeFormatCustomization.java |   51 -
 .../main/org/apache/sis/util/internal/URLs.java    |   64 --
 .../sis/util/internal/UnmodifiableArrayList.java   |  439 ---------
 .../main/org/apache/sis/util/internal/Unsafe.java  |  106 --
 .../main/org/apache/sis/util/internal/X364.java    |  303 ------
 .../org/apache/sis/util/internal/package-info.java |    8 +-
 .../apache/sis/util/logging/MonolineFormatter.java |    6 +-
 .../apache/sis/util/privy/AbstractIterator.java    |   63 ++
 .../org/apache/sis/util/privy/AbstractMap.java     |  709 ++++++++++++++
 .../apache/sis/util/privy/AbstractMapEntry.java    |  100 ++
 .../main/org/apache/sis/util/privy/Bag.java        |   99 ++
 .../apache/sis/util/privy/CheckedArrayList.java    |  324 +++++++
 .../org/apache/sis/util/privy/CheckedHashSet.java  |  115 +++
 .../org/apache/sis/util/privy/CloneAccess.java     |   46 +
 .../main/org/apache/sis/util/privy/Cloner.java     |  267 +++++
 .../main/org/apache/sis/util/privy/CodeLists.java  |  169 ++++
 .../org/apache/sis/util/privy/CollectionsExt.java  |  880 +++++++++++++++++
 .../main/org/apache/sis/util/privy/Constants.java  |  266 +++++
 .../org/apache/sis/util/privy/DefinitionURI.java   |  749 ++++++++++++++
 .../org/apache/sis/util/privy/DoubleDouble.java    |  918 ++++++++++++++++++
 .../main/org/apache/sis/util/privy/EmptyQueue.java |   68 ++
 .../apache/sis/util/privy/ListOfUnknownSize.java   |  259 +++++
 .../sis/util/privy/LocalizedParseException.java    |  195 ++++
 .../apache/sis/util/privy/MetadataServices.java    |  186 ++++
 .../main/org/apache/sis/util/privy/Numerics.java   |  648 +++++++++++++
 .../org/apache/sis/util/privy/PropertyFormat.java  |  238 +++++
 .../apache/sis/util/privy/SetOfUnknownSize.java    |  199 ++++
 .../apache/sis/util/privy/StandardDateFormat.java  |  513 ++++++++++
 .../main/org/apache/sis/util/privy/Strings.java    |  420 ++++++++
 .../sis/util/privy/TreeFormatCustomization.java    |   51 +
 .../main/org/apache/sis/util/privy/URLs.java       |   64 ++
 .../sis/util/privy/UnmodifiableArrayList.java      |  439 +++++++++
 .../main/org/apache/sis/util/privy/Unsafe.java     |  106 ++
 .../main/org/apache/sis/util/privy/X364.java       |  303 ++++++
 .../org/apache/sis/util/privy/package-info.java    |   36 +
 .../sis/util/resources/IndexedResourceBundle.java  |    4 +-
 .../org/apache/sis/util/resources/Messages.java    |    5 +
 .../apache/sis/util/resources/Messages.properties  |    1 +
 .../sis/util/resources/Messages_fr.properties      |    1 +
 .../apache/sis/util/resources/package-info.java    |    4 +-
 .../test/org/apache/sis/io/WordWrapTest.java       |    2 +-
 .../org/apache/sis/measure/RangeFormatTest.java    |    2 +-
 .../test/org/apache/sis/test/TestCase.java         |    2 +-
 .../test/org/apache/sis/test/TestUtilities.java    |    4 +-
 .../org/apache/sis/util/collection/CacheTest.java  |    2 +-
 .../apache/sis/util/collection/RangeSetTest.java   |    4 +-
 .../apache/sis/util/internal/AbstractMapTest.java  |  191 ----
 .../test/org/apache/sis/util/internal/BagTest.java |   84 --
 .../sis/util/internal/CheckedArrayListTest.java    |  146 ---
 .../sis/util/internal/CollectionsExtTest.java      |  175 ----
 .../sis/util/internal/DefinitionURITest.java       |  270 ------
 .../apache/sis/util/internal/DoubleDoubleTest.java |  410 --------
 .../sis/util/internal/ListOfUnknownSizeTest.java   |   64 --
 .../util/internal/LocalizedParseExceptionTest.java |   76 --
 .../org/apache/sis/util/internal/NumericsTest.java |  242 -----
 .../sis/util/internal/StandardDateFormatTest.java  |  148 ---
 .../org/apache/sis/util/internal/StringsTest.java  |   57 --
 .../org/apache/sis/util/internal/X364Test.java     |   69 --
 .../sis/util/logging/MonolineFormatterTest.java    |    2 +-
 .../org/apache/sis/util/privy/AbstractMapTest.java |  191 ++++
 .../test/org/apache/sis/util/privy/BagTest.java    |   84 ++
 .../sis/util/privy/CheckedArrayListTest.java       |  146 +++
 .../apache/sis/util/privy/CollectionsExtTest.java  |  175 ++++
 .../apache/sis/util/privy/DefinitionURITest.java   |  270 ++++++
 .../apache/sis/util/privy/DoubleDoubleTest.java    |  410 ++++++++
 .../sis/util/privy/ListOfUnknownSizeTest.java      |   64 ++
 .../util/privy/LocalizedParseExceptionTest.java    |   76 ++
 .../org/apache/sis/util/privy/NumericsTest.java    |  242 +++++
 .../sis/util/privy/StandardDateFormatTest.java     |  148 +++
 .../org/apache/sis/util/privy/StringsTest.java     |   57 ++
 .../test/org/apache/sis/util/privy/X364Test.java   |   69 ++
 .../storage/shapefile/ListingPropertyVisitor.java  |    4 +-
 .../sis/storage/shapefile/ShapefileStore.java      |    4 +-
 .../sis/storage/shapefile/ShapefileStoreTest.java  |    2 +-
 netbeans-project/nbproject/project.properties      |    6 +-
 .../apache/sis/gui/controls/SyncWindowList.java    |    2 +-
 .../apache/sis/gui/controls/ValueColorMapper.java  |    2 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    |    2 +-
 .../org/apache/sis/gui/coverage/GridTileCache.java |    2 +-
 .../main/org/apache/sis/gui/coverage/GridView.java |    2 +-
 .../apache/sis/gui/coverage/IsolineRenderer.java   |    2 +-
 .../org/apache/sis/gui/dataset/ExpandableList.java |    2 +-
 .../org/apache/sis/gui/dataset/FeatureList.java    |    2 +-
 .../org/apache/sis/gui/dataset/FeatureTable.java   |    4 +-
 .../org/apache/sis/gui/dataset/ResourceCell.java   |    2 +-
 .../apache/sis/gui/internal/DataStoreOpener.java   |    2 +-
 .../org/apache/sis/gui/internal/GUIUtilities.java  |    2 +-
 .../apache/sis/gui/internal/ImageConverter.java    |    6 +-
 .../sis/gui/internal/PropertyValueFormats.java     |    2 +-
 .../sis/gui/internal/PropertyValueFormatter.java   |    2 +-
 .../apache/sis/gui/internal/io/FileAccessItem.java |    2 +-
 .../main/org/apache/sis/gui/map/MapCanvas.java     |    8 +-
 .../main/org/apache/sis/gui/map/MapCanvasAWT.java  |    2 +-
 .../main/org/apache/sis/gui/map/StatusBar.java     |    2 +-
 .../org/apache/sis/gui/map/ValuesFormatter.java    |    2 +-
 .../sis/gui/metadata/IdentificationInfo.java       |    4 +-
 .../apache/sis/gui/metadata/MetadataSummary.java   |    2 +-
 .../sis/gui/metadata/RepresentationInfo.java       |    2 +-
 .../sis/gui/metadata/StandardMetadataTree.java     |    2 +-
 .../apache/sis/gui/referencing/AuthorityCodes.java |    4 +-
 .../org/apache/sis/gui/referencing/CRSChooser.java |    2 +-
 .../org/apache/sis/gui/referencing/CodeFilter.java |    2 +-
 .../org/apache/sis/gui/referencing/MenuSync.java   |    2 +-
 .../gui/referencing/PositionableProjection.java    |    4 +-
 .../gui/referencing/RecentReferenceSystems.java    |    4 +-
 .../main/org/apache/sis/gui/referencing/Utils.java |    2 +-
 1261 files changed, 45217 insertions(+), 45134 deletions(-)

diff --cc endorsed/src/org.apache.sis.feature/main/module-info.java
index 519d3be0f3,6daf162d76..80ebe49fda
--- a/endorsed/src/org.apache.sis.feature/main/module-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/module-info.java
@@@ -45,14 -45,10 +45,14 @@@ module org.apache.sis.feature 
              org.apache.sis.storage,
              org.apache.sis.storage.sql,
              org.apache.sis.storage.shapefile,       // In the "incubator" sub-project.
 -            org.apache.sis.cql,                     // In the "incubator" sub-project.
 -            org.apache.sis.portrayal.map;           // In the "incubator" sub-project.
 +            org.apache.sis.portrayal;
 +
-     exports org.apache.sis.filter.internal to
++    exports org.apache.sis.filter.privy to
 +            org.apache.sis.storage,
 +            org.apache.sis.storage.sql,
 +            org.apache.sis.storage.shapefile;       // In the "incubator" sub-project.
  
-     exports org.apache.sis.feature.internal to
+     exports org.apache.sis.feature.privy to
              org.apache.sis.storage,
              org.apache.sis.storage.xml,
              org.apache.sis.storage.netcdf,
@@@ -70,12 -69,11 +70,7 @@@
      exports org.apache.sis.geometry.wrapper.j2d to
              org.apache.sis.gui;                     // In the "optional" sub-project.
  
-     exports org.apache.sis.coverage.internal to
-             org.apache.sis.storage,
-             org.apache.sis.storage.netcdf,
-             org.apache.sis.portrayal;
 -    exports org.apache.sis.geometry.wrapper.jts to
 -            org.apache.sis.portrayal.map,           // In the "incubator" sub-project.
 -            org.apache.sis.cql;                     // In the "incubator" sub-project.
--
-     exports org.apache.sis.coverage.grid.j2d to
+     exports org.apache.sis.coverage.privy to
              org.apache.sis.storage,
              org.apache.sis.storage.sql,
              org.apache.sis.storage.netcdf,
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CoverageCombiner.java
index 4cde54a13b,1834312069..bc3974f7b4
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CoverageCombiner.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/CoverageCombiner.java
@@@ -39,10 -39,13 +39,10 @@@ import org.apache.sis.image.Interpolati
  import org.apache.sis.image.PlanarImage;
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.measure.NumberRange;
- import org.apache.sis.coverage.internal.SampleDimensions;
- import static org.apache.sis.util.internal.Numerics.saturatingAdd;
- import static org.apache.sis.util.internal.Numerics.saturatingSubtract;
+ import org.apache.sis.coverage.privy.SampleDimensions;
+ import static org.apache.sis.util.privy.Numerics.saturatingAdd;
+ import static org.apache.sis.util.privy.Numerics.saturatingSubtract;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.CannotEvaluateException;
 -
  
  /**
   * Combines an arbitrary number of coverages into a single one.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
index 46ca35a33b,657f765cdc..e744a78197
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
@@@ -26,11 -26,12 +26,11 @@@ import org.opengis.referencing.crs.Coor
  import org.opengis.referencing.datum.PixelInCell;
  import org.apache.sis.feature.internal.Resources;
  import org.apache.sis.util.StringBuilders;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.PointOutsideCoverageException;
 -import org.opengis.coverage.grid.GridCoordinates;
 +// Specific to the main branch:
 +import org.apache.sis.coverage.PointOutsideCoverageException;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/CommonDomainFinder.java
index 0000000000,6d2d581f8c..bc7e23ebe4
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/CommonDomainFinder.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/privy/CommonDomainFinder.java
@@@ -1,0 -1,374 +1,383 @@@
+ /*
+  * 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.coverage.privy;
+ 
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.NoSuchElementException;
+ import org.opengis.util.FactoryException;
+ import org.opengis.geometry.MismatchedDimensionException;
+ import org.opengis.referencing.datum.PixelInCell;
+ import org.opengis.referencing.operation.Matrix;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.TransformException;
+ import org.opengis.referencing.operation.NoninvertibleTransformException;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.apache.sis.referencing.CRS;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.coverage.grid.GridExtent;
+ import org.apache.sis.coverage.grid.GridGeometry;
+ import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+ import org.apache.sis.referencing.privy.ExtendedPrecisionMatrix;
+ import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.util.Numbers;
+ import org.apache.sis.util.privy.Numerics;
+ 
+ 
+ /**
+  * Helper class for building a combined domain from a list of grid geometries.
+  * After construction, one of the following methods shall be invoked exactly once.
+  *
+  * <ul>
+  *   <li>{@link #setFromGridAligned(GridGeometry...)}</li>
+  * </ul>
+  *
+  * Then, the result can be obtained by the given methods:
+  *
+  * <ul>
+  *   <li>{@link #result()}</li>
+  *   <li>{@link #gridTranslations()}</li>
+  *   <li>{@link #sourceOfGridToCRS()}</li>
+  * </ul>
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class CommonDomainFinder {
+     /**
+      * Whether to compute intersection (true) or union (false) of all grid coverages.
+      * This is not yet configurable in current version.
+      *
+      * <p>We use this constant for tracking the code to update when we will want to provide an option for using
+      * union or strict equality instead (the latter would be a mode that fails if all extents are not identical).
+      * Note that in the case of unions, it would be possible to specify coverages with no intersection.
+      * Whether we should accept that or raise an exception is still an open question.</p>
+      */
+     public static final boolean INTERSECTION = true;
+ 
+     /**
+      * The grid geometry taken as the reference for computing the translations of all grid geometry items.
+      * Values of {@link #gridTranslations} and {@link #itemTranslations} are relative to that reference.
+      * At first this is an arbitrary grid geometry, for example the first encountered one.
+      * After {@code CommonDomainFinder} completed its task, this is updated to the final result.
+      *
+      * @see #result()
+      */
+     private GridGeometry reference;
+ 
+     /**
+      * Coordinate reference system of the reference, or {@code null} if not yet known.
+      * It will be set to the CRS of the first grid geometry where the CRS is defined.
+      */
+     private CoordinateReferenceSystem crs;
+ 
+     /**
+      * The inverse of the "grid to CRS" transform of the grid geometry taken as a reference.
+      */
+     private MathTransform crsToGrid;
+ 
+     /**
+      * The convention to use for fetching the "grid to CRS" transforms.
+      */
+     private final PixelInCell anchor;
+ 
+     /**
+      * The combined extent, as the union or intersection of all grid extents.
+      */
+     private GridExtent extent;
+ 
+     /**
+      * The translation in units of grid cells from the {@linkplain #reference} grid geometry
+      * to the grid geometry in the key.
+      */
+     private final Map<GridGeometry,long[]> gridTranslations;
+ 
+     /**
+      * Translations in units of grid cells from the {@linkplain #reference} grid geometry to each item.
+      * For each index <var>i</var>, {@code itemTranslations[i]} is a value from {@link #gridTranslations}
+      * map and may be reused at more than one index <var>i</var>.
+      *
+      * @see #gridTranslations()
+      */
+     private long[][] itemTranslations;
+ 
+     /**
+      * If one of the grid geometries has the same "grid to CRS" than the common grid geometry, the index.
+      * Otherwise -1.
+      */
+     private int sourceOfGridToCRS;
+ 
+     /**
+      * Creates a new common domain finder.
+      *
+      * @param  anchor  the convention to use for fetching the "grid to CRS" transforms.
+      */
+     CommonDomainFinder(final PixelInCell anchor) {
+         this.anchor = anchor;
+         gridTranslations = new LinkedHashMap<>();
+     }
+ 
+     /**
+      * Computes a common grid geometry from the given items.
+      * All items shall share be aligned on the same grid.
+      * Items may be translated relative to each other,
+      * but the translations shall be an integer number of grid cells.
+      *
+      * <h4>Coordinate reference system</h4>
+      * If the CRS of a grid geometry is undefined, it is assumed the same as other grid geometries.
+      *
+      * @param  items  the grid geometries for which to compute a common grid geometry.
+      * @throws IllegalGridGeometryException if the specified item is not compatible with the reference grid geometry.
+      */
+     final void setFromGridAligned(final GridGeometry... items) {
+         itemTranslations = new long[items.length][];
+         for (int i=0; i<items.length; i++) {
+             itemTranslations[i] = gridTranslations.computeIfAbsent(items[i], this::itemToCommon);
+         }
+         /*
+          * Change the reference grid geometry for matching more closely the desired grid extent.
+          * If one item has exactly the desired grid extent, use it. Otherwise search for an item
+          * having the same origin. This criterion is arbitrary and may change in future version.
+          */
+         GridGeometry fallback = null;
+         GridExtent   location = null;
+         for (final Map.Entry<GridGeometry,long[]> entry : gridTranslations.entrySet()) {
+             final GridGeometry item   = entry.getKey();
+             final GridExtent actual   = item.getExtent();
+             final GridExtent expected = extent.translate(entry.getValue());
+             if (actual.equals(expected)) {
+                 setGridToCRS(items, item);      // Must be before `reference` assignation.
+                 reference = item;
+                 return;
+             }
+             // Arbitrary criterion (may be revisited in any future version).
 -            if (fallback == null && expected.getLow().equals(actual.getLow())) {
++            if (fallback == null && sameLow(expected, actual)) {
+                 location = expected;
+                 fallback = item;
+             }
+         }
+         if (fallback == null) {
+             fallback = reference;
+             location = extent;
+         }
+         setGridToCRS(items, fallback);
+         try {
+             reference = fallback.relocate(location);
+         } catch (TransformException e) {
+             throw new IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries), e);
+         }
+     }
+ 
++    private static boolean sameLow(final GridExtent e1, final GridExtent e2) {
++        final int dim = e1.getDimension();
++        if (dim != e2.getDimension()) return false;
++        for (int i=0; i<dim; i++) {
++            if (e1.getLow(i) != e2.getLow(i)) return false;
++        }
++        return true;
++    }
++
+     /**
+      * Given a grid geometry with the desired "grid to CRS", saves its index in {@link #sourceOfGridToCRS}.
+      * This method updates all previously computed translations for making them relative to the new reference.
+      *
+      * Note: updating values of the {@link #gridTranslations} map indirectly update all
+      * {@link #itemTranslations} array elements.
+      */
+     private void setGridToCRS(final GridGeometry[] items, final GridGeometry item) {
+         sourceOfGridToCRS = indexOf(items, item);
+         if (item == reference) {                    // Quick check for a common case.
+             return;
+         }
+         final long[] oldReference = itemTranslations[indexOf(items, reference)];
+         final long[] newReference = itemTranslations[sourceOfGridToCRS];
+         final long[] change = new long[newReference.length];
+         for (int i=0; i < newReference.length; i++) {
+             change[i] = Math.subtractExact(newReference[i], oldReference[i]);
+         }
+         for (final long[] offset : gridTranslations.values()) {
+             for (int i=0; i < offset.length; i++) {
+                 offset[i] = Math.subtractExact(offset[i], change[i]);
+             }
+         }
+     }
+ 
+     /**
+      * Returns the index of the given grid geometry.
+      * This method is invoked when the instance should always exist in the array.
+      */
+     private static int indexOf(final GridGeometry[] items, final GridGeometry item) {
+         for (int i=0; i<items.length; i++) {
+             if (items[i] == item) {
+                 return i;
+             }
+         }
+         throw new NoSuchElementException();         // Should never happen.
+     }
+ 
+     /**
+      * Returns the common grid geometry computed from all specified items.
+      *
+      * @return a grid geometry which is the union or intersection of all specified items.
+      */
+     final GridGeometry result() {
+         if (crs != null && !reference.isDefined(GridGeometry.CRS)) {
+             reference = new GridGeometry(extent, anchor, reference.getGridToCRS(anchor), crs);
+         }
+         return reference;
+     }
+ 
+     /**
+      * Returns the translations (in units of grid cells) from the common grid geometry to all items.
+      * The items are the arguments given to {@link #setFromGridAligned(GridGeometry...)}, in order.
+      * The common grid geometry is the value returned by {@link #result()}.
+      *
+      * <p>The returned array should not be modified because it is not cloned.</p>
+      *
+      * @return translation from the common grid geometry to all items. This array is not cloned.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     final long[][] gridTranslations() {
+         return itemTranslations;
+     }
+ 
+     /**
+      * If one items has the same "grid to CRS" than the common grid geometry, returns its index.
+      *
+      * @return index of an items having the desired "grid to CRS", or -1 if none.
+      */
+     final int sourceOfGridToCRS() {
+         return sourceOfGridToCRS;
+     }
+ 
+     /**
+      * Computes the translation in units of grid cells from the common grid geometry to the given item.
+      * This method also opportunistically computes the union or intersection of all grid extents.
+      *
+      * @param  item  the grid geometry for which to get a translation from the common grid geometry.
+      * @return translation in unis of grid cells. Note that the caller may reuse this array for many grid geometries.
+      * @throws IllegalGridGeometryException if the specified item is not compatible with the reference grid geometry.
+      */
+     private long[] itemToCommon(final GridGeometry item) {
+         /*
+          * Compute the change ourselves instead of invoking `GridGeometry.createTransformTo(…)`
+          * because we do not want wraparound handling when we search for a simple translation.
+          */
+         MathTransform change = item.getGridToCRS(anchor);
+         try {
+             if (crsToGrid == null) {
+                 crsToGrid = change.inverse();
+                 reference = item;
+             }
+             if (item.isDefined(GridGeometry.CRS)) {
+                 final CoordinateReferenceSystem src = item.getCoordinateReferenceSystem();
+                 if (crs == null) {
+                     crs = src;
+                 } else {
+                     /*
+                      * Ask for a change of CRS without specifying an area of interest (AOI) on the assumption
+                      * that if the transformation is only a translation, the AOI would not make a difference.
+                      * It save not only the AOI computation cost, but also make easier for `findOperation(…)`
+                      * to use its cache.
+                      */
+                     change = MathTransforms.concatenate(change, CRS.findOperation(src, crs, null).getMathTransform());
+                 }
+             }
+             change = MathTransforms.concatenate(change, crsToGrid);
+         } catch (FactoryException | NoninvertibleTransformException | MismatchedDimensionException e) {
+             throw new IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries), e);
+         }
+         final long[] offset = integerTranslation(MathTransforms.getMatrix(change), null);
+         if (offset == null) {
+             throw new IllegalGridGeometryException(Resources.format(Resources.Keys.IncompatibleGridGeometries));
+         }
+         /*
+          * The grid geometry has been accepted as valid. Now compute the combined extent,
+          * taking offset in account. At this point this is an offset TO the common grid geometry.
+          * It will be converted to an offset FROM the common grid geometry after the extent update.
+          */
+         final GridExtent e = item.getExtent().translate(offset);
+         if (extent == null) {
+             extent = e;
+         } else if (INTERSECTION) {
+             extent = extent.intersect(e);
+         } else if (!extent.equals(e)) {
+             throw new IllegalGridGeometryException();
+         }
+         for (int i=0; i<offset.length; i++) {
+             offset[i] = Math.negateExact(offset[i]);
+         }
+         return offset;
+     }
+ 
+     /**
+      * If the given matrix is the identity matrix except for translation terms, returns the translation.
+      * Translation terms must be integer values and will be stored in the given {@code offset} array.
+      * If the matrix is not an integer translation, this method return {@code null} without modifying
+      * the given {@code offset} array.
+      *
+      * @param  change  conversion between two grid geometries, or {@code null}.
+      * @param  offset  where to store translation terms if the change is an integer translation, or {@code null}.
+      * @return the translation terms, or {@code null} if the given matrix does not met the conditions.
+      *
+      * @see org.apache.sis.referencing.operation.matrix.Matrices#isTranslation(Matrix)
+      */
+     public static long[] integerTranslation(final Matrix change, long[] offset) {
+         if (change == null) {
+             return null;
+         }
+         final int numRows = change.getNumRow();
+         final int numCols = change.getNumCol();
+         for (int j=0; j<numRows; j++) {
+             for (int i=0; i<numCols; i++) {
+                 double tolerance = Numerics.COMPARISON_THRESHOLD;
+                 double e = change.getElement(j, i);
+                 if (i == j) {
+                     e--;
+                 } else if (i == numCols - 1) {
+                     final double a = Math.abs(e);
+                     if (a > 1) {
+                         tolerance = Math.min(tolerance*a, 0.125);
+                     }
+                     e -= Math.rint(e);
+                 }
+                 if (!(Math.abs(e) <= tolerance)) {      // Use `!` for catching NaN.
+                     return null;
+                 }
+             }
+         }
+         /*
+          * Store the translation terms after we have determined that they are integers.
+          * It must be an "all or nothing" operation (unless an exception is thrown).
+          */
+         if (offset == null) {
+             offset = new long[numRows - 1];
+         }
+         final int i = numCols - 1;
+         if (change instanceof ExtendedPrecisionMatrix) {
+             final var epm = (ExtendedPrecisionMatrix) change;
+             for (int j=0; j<offset.length; j++) {
+                 final Number e = epm.getElementOrNull(j, i);
+                 offset[j] = (e != null) ? Numbers.round(e) : 0;
+             }
+         } else {
+             for (int j=0; j<offset.length; j++) {
+                 offset[j] = Math.round(change.getElement(j, i));
+             }
+         }
+         return offset;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
index f3c6fbf2bd,c073f80a18..14f769111d
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
@@@ -30,9 -30,23 +30,9 @@@ import org.apache.sis.util.ArgumentChec
  import org.apache.sis.util.CorruptedObjectException;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.util.collection.Containers;
- import org.apache.sis.util.internal.CheckedArrayList;
+ import org.apache.sis.util.privy.CheckedArrayList;
  import org.apache.sis.feature.internal.Resources;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Property;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.feature.InvalidPropertyValueException;
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.FeatureAssociation;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Operation;
 -
  
  /**
   * An instance of a {@linkplain DefaultFeatureType feature type} containing values for a real-world phenomena.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/CharacteristicMap.java
index 1008b52f1d,8c32f92b3a..8b579e0324
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/CharacteristicMap.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/CharacteristicMap.java
@@@ -18,12 -18,18 +18,12 @@@ package org.apache.sis.feature
  
  import java.util.Map;
  import org.opengis.util.GenericName;
- import org.apache.sis.util.internal.Cloner;
- import org.apache.sis.util.internal.CloneAccess;
- import org.apache.sis.util.internal.AbstractMap;
- import org.apache.sis.util.internal.AbstractMapEntry;
+ import org.apache.sis.util.privy.Cloner;
+ import org.apache.sis.util.privy.CloneAccess;
+ import org.apache.sis.util.privy.AbstractMap;
+ import org.apache.sis.util.privy.AbstractMapEntry;
  import org.apache.sis.feature.internal.Resources;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.InvalidPropertyValueException;
 -import org.opengis.feature.PropertyNotFoundException;
 -
  
  /**
   * Implementation of {@link AbstractAttribute#characteristics()} map.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
index a2ace425b1,ab1fbb4b57..fd126ca70b
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
@@@ -26,8 -26,16 +26,8 @@@ import org.opengis.util.GenericName
  import org.opengis.util.InternationalString;
  import org.opengis.metadata.Identifier;
  import org.apache.sis.feature.internal.Resources;
- import org.apache.sis.feature.internal.AttributeConvention;
+ import org.apache.sis.feature.privy.AttributeConvention;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.FeatureAssociation;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.PropertyNotFoundException;
 -
  
  /**
   * Indicates the role played by the association between two features.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
index 7a942b459e,772e341d96..d8fe6432f3
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
@@@ -36,10 -36,21 +36,10 @@@ import org.opengis.util.InternationalSt
  import org.opengis.parameter.ParameterDescriptorGroup;
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.util.collection.Containers;
- import org.apache.sis.util.internal.CollectionsExt;
- import org.apache.sis.util.internal.UnmodifiableArrayList;
+ import org.apache.sis.util.privy.CollectionsExt;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
  import org.apache.sis.feature.internal.Resources;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.FeatureInstantiationException;
 -import org.opengis.feature.PropertyNotFoundException;
 -
  
  /**
   * Abstraction of a real-world phenomena. A {@code FeatureType} instance describes the class of all
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
index 70ef332a83,6cdfb9c7e7..bbdfb52a4a
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
@@@ -21,9 -21,15 +21,9 @@@ import java.util.Arrays
  import org.opengis.metadata.maintenance.ScopeCode;
  import org.opengis.metadata.quality.DataQuality;
  import org.apache.sis.util.ArgumentChecks;
- import org.apache.sis.util.internal.CloneAccess;
- import org.apache.sis.util.internal.Cloner;
+ import org.apache.sis.util.privy.CloneAccess;
+ import org.apache.sis.util.privy.Cloner;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Property;
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.FeatureAssociation;
 -import org.opengis.feature.PropertyNotFoundException;
 -
  
  /**
   * A feature in which most properties are expected to be provided. This implementation uses a plain array for
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
index 329dc3ed22,3518b6b10d..9d56804430
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
@@@ -23,15 -23,20 +23,15 @@@ import java.util.Collection
  import java.util.function.Function;
  import org.opengis.parameter.ParameterValueGroup;
  import org.opengis.parameter.ParameterDescriptorGroup;
- import org.apache.sis.feature.internal.FeatureUtilities;
- import org.apache.sis.filter.internal.FunctionNames;
- import org.apache.sis.filter.internal.Visitor;
+ import org.apache.sis.feature.privy.FeatureUtilities;
+ import org.apache.sis.filter.privy.FunctionNames;
+ import org.apache.sis.filter.privy.Visitor;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.CodeList;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.Property;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.pending.geoapi.filter.LogicalOperator;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index 0daba2f8aa,f88ee704ac..ef10b003fa
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@@ -27,11 -27,16 +27,11 @@@ import org.apache.sis.util.Static
  import org.apache.sis.util.UnconvertibleObjectException;
  import org.apache.sis.util.collection.WeakHashSet;
  import org.apache.sis.util.resources.Errors;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  import org.apache.sis.setup.GeometryLibrary;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.filter.Expression;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
index 79f4648ec3,6b99151a2c..f5efb4ae70
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
@@@ -21,9 -21,15 +21,9 @@@ import java.util.Map
  import java.io.IOException;
  import org.opengis.parameter.ParameterValueGroup;
  import org.opengis.parameter.ParameterDescriptorGroup;
- import org.apache.sis.feature.internal.FeatureUtilities;
+ import org.apache.sis.feature.privy.FeatureUtilities;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Property;
 -import org.opengis.feature.PropertyType;
 -
  
  /**
   * A link operation, which is like a redirection or an alias.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/MultiValuedAssociation.java
index d06569c32a,1f9b497dc6..ecca415815
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/MultiValuedAssociation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/MultiValuedAssociation.java
@@@ -18,9 -18,15 +18,9 @@@ package org.apache.sis.feature
  
  import java.util.Collection;
  import org.apache.sis.util.ArgumentChecks;
- import org.apache.sis.util.internal.CheckedArrayList;
+ import org.apache.sis.util.privy.CheckedArrayList;
  import org.apache.sis.feature.internal.Resources;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.MultiValuedPropertyException;
 -
  
  /**
   * An instance of an {@linkplain DefaultAssociationRole association role} containing an arbitrary number of values.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
index c6e8e439aa,abc94cb28d..540e63e97b
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
@@@ -20,8 -20,18 +20,8 @@@ import java.util.Collection
  import java.util.Collections;
  import java.io.Serializable;
  import org.opengis.util.GenericName;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import java.util.Set;
 -import org.opengis.util.InternationalString;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.feature.FeatureInstantiationException;
 -import org.apache.sis.feature.internal.Resources;
 -
  
  /**
   * A feature type identified only by its name. Instances of {@code NamedFeatureType} shall be used only as placeholder
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SingletonAttribute.java
index dbe9774856,8175cd608a..2ef54b20b7
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SingletonAttribute.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SingletonAttribute.java
@@@ -17,8 -17,11 +17,8 @@@
  package org.apache.sis.feature;
  
  import java.util.Objects;
- import org.apache.sis.util.internal.CloneAccess;
+ import org.apache.sis.util.privy.CloneAccess;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -
  
  /**
   * An instance of an {@linkplain DefaultAttributeType attribute type} containing at most one value.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
index 6eba9707f0,8fdb173a5f..21ce42eade
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
@@@ -24,9 -24,15 +24,9 @@@ import org.opengis.metadata.maintenance
  import org.opengis.metadata.quality.DataQuality;
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.util.CorruptedObjectException;
- import org.apache.sis.util.internal.CloneAccess;
- import org.apache.sis.util.internal.Cloner;
+ import org.apache.sis.util.privy.CloneAccess;
+ import org.apache.sis.util.privy.Cloner;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Property;
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.FeatureAssociation;
 -import org.opengis.feature.PropertyNotFoundException;
 -
  
  /**
   * A feature in which only a small fraction of properties are expected to be provided. This implementation uses
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/AttributeConvention.java
index 0000000000,1e2c9030dc..150d5afad5
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/AttributeConvention.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/AttributeConvention.java
@@@ -1,0 -1,444 +1,440 @@@
+ /*
+  * 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.feature.privy;
+ 
+ import java.util.Optional;
+ import org.opengis.util.LocalName;
+ import org.opengis.util.ScopedName;
+ import org.opengis.util.GenericName;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.apache.sis.util.Static;
+ import org.apache.sis.util.iso.Names;
+ import org.apache.sis.feature.Features;
+ import org.apache.sis.geometry.wrapper.Geometries;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.Property;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyNotFoundException;
++// Specific to the main branch:
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.AbstractAttribute;
++import org.apache.sis.feature.AbstractIdentifiedType;
++import org.apache.sis.feature.DefaultAttributeType;
++import org.apache.sis.feature.DefaultFeatureType;
+ 
+ 
+ /**
+  * Defines the names of some properties or characteristics for which we assign a conventional usage.
+  * Properties with the names defined in this {@code AttributeConvention} class are often aliases generated
+  * by the SIS implementation of various file readers. Those synthetic properties redirect to the most
+  * appropriate "real" property in the feature.
+  *
+  * <div class="note"><b>Example:</b>
+  * one of the most frequently used synthetic property is {@code "sis:identifier"}, which contains a unique
+  * identifier (or primary key) for the feature. This property is usually (but not necessarily)
+  * a {@linkplain org.apache.sis.feature.FeatureOperations#link link to an existing attribute}.
+  * By using the {@code "sis:identifier"} alias, users do not need to know the name of the "real" attribute.
+  * </div>
+  *
+  * This class defines names for two kinds of usage:
+  * <ul>
+  *   <li>Names ending with {@code "_PROPERTY"} are used for attributes or operations that are members of the
+  *       collection returned by {@link org.apache.sis.feature.DefaultFeatureType#getProperties(boolean)}.</li>
+  *   <li>Names ending with {@code "_CHARACTERISTIC"} are used for characteristics that are entries of the
+  *       map returned by {@link org.apache.sis.feature.DefaultAttributeType#characteristics()}.</li>
+  * </ul>
+  *
+  * <h2>Mixing with other conventions</h2>
+  * The conventions defined in this class are specific to Apache SIS.
+  * Current implementation does not support any other convention than the SIS one,
+  * but we may refactor this class in future SIS versions if there is a need to support different conventions.
+  *
+  * <p>In order to reduce the risk of name collision with properties in user-defined features
+  * (e.g. the user may already have an attribute named {@code "identifier"} for his own purpose),
+  * all names defined in this class begin with the {@code "@"} character.</p>
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class AttributeConvention extends Static {
+     /**
+      * Scope of all names defined by SIS convention.
+      */
+     private static final LocalName SCOPE = Names.createLocalName("Apache", null, "sis");
+ 
+     /**
+      * Conventional name for a property used as a unique identifier.
+      * The identifier should be unique in the {@link org.apache.sis.storage.DataStore} instance containing the feature
+      * (for example a {@code DataStore} opened for a XML file), but does not need to be unique between two independent
+      * {@code DataStore} instances.
+      *
+      * <p>Properties of this name are usually
+      * {@linkplain org.apache.sis.feature.FeatureOperations#link aliases for existing attributes}, or
+      * {@linkplain org.apache.sis.feature.FeatureOperations#compound compound keys} made by concatenation
+      * of two or more other attributes.</p>
+      *
+      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} is usually
+      * {@link String}, {@link Integer}, {@link java.util.UUID} or other types commonly used as identifiers.</p>
+      */
+     public static final ScopedName IDENTIFIER_PROPERTY = Names.createScopedName(SCOPE, null, "identifier");
+ 
+     /**
+      * Conventional name for a property containing the geometric object to use by default.
+      * Some features may contain more than one geometric object; this property tells which
+      * geometry to render on a map for example.
+      *
+      * <p>Properties of this name are usually {@linkplain org.apache.sis.feature.FeatureOperations#link
+      * operations acting as a redirection to another attribute}.</p>
+      *
+      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} can be
+      * the {@link com.esri.core.geometry.Geometry} class from ESRI's API, or the {@code Geometry} class from
+      * <cite>Java Topology Suite</cite> (JTS) library, or any other class defined in future SIS versions.
 -     * See {@link #isGeometryAttribute(IdentifiedType)} for testing whether the value is a supported type.</p>
++     * See {@code isGeometryAttribute(IdentifiedType)} for testing whether the value is a supported type.</p>
+      *
 -     * @see #isGeometryAttribute(IdentifiedType)
++     * @see #isGeometryAttribute(AbstractIdentifiedType)
+      */
+     public static final ScopedName GEOMETRY_PROPERTY = Names.createScopedName(SCOPE, null, "geometry");
+ 
+     /**
+      * Conventional name for fetching the envelope encompassing all geometries in a feature.
+      * Most {@code FeatureType}s have at most one geometry, which is also the {@link #GEOMETRY_PROPERTY default geometry}.
+      * But if several geometries exist, then the value for this synthetic property is the union of all geometries.
+      *
+      * <p>Properties of this name are usually
+      * {@linkplain org.apache.sis.feature.FeatureOperations#envelope operations}.</p>
+      *
+      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} should be
+      * {@link org.opengis.geometry.Envelope}.</p>
+      */
+     public static final ScopedName ENVELOPE_PROPERTY = Names.createScopedName(SCOPE, null, "envelope");
+ 
+     /**
+      * Conventional name for fetching the Coordinate Reference System (CRS) of a geometry or a coverage.
+      * This characteristic is typically an entry in the map returned by a call to the
+      * {@link org.apache.sis.feature.DefaultAttributeType#characteristics()} method
+      * on the attribute referenced by {@link #GEOMETRY_PROPERTY}.
+      *
+      * <p>While it is technically possible to have different CRS for different feature instances,
+      * in most cases the CRS is the same for all geometries found in {@code GEOMETRY_PROPERTY}.
+      * In such cases, the CRS can be specified only once as the
+      * {@linkplain org.apache.sis.feature.DefaultAttributeType#getDefaultValue() default value}
+      * of this {@code CRS_CHARACTERISTIC}.</p>
+      *
+      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} should be
+      * {@link org.opengis.referencing.crs.CoordinateReferenceSystem}.</p>
+      *
 -     * @see #getCRSCharacteristic(Property)
++     * @see #getCRSCharacteristic(Object)
+      */
+     public static final ScopedName CRS_CHARACTERISTIC = Names.createScopedName(SCOPE, null, "crs");
+ 
+     /**
+      * Conventional name for fetching the unit of measurement of a property.
+      * This characteristic is typically an entry in the map returned by a call to the
+      * {@link org.apache.sis.feature.DefaultAttributeType#characteristics()} method
+      * on any attribute of numeric type.
+      *
+      * <p>While it is technically possible to have different units of measurement for the same property
+      * on different feature instances, in most cases the unit is the same for all feature instances.
+      * In such cases, the unit can be specified only once as the
+      * {@linkplain org.apache.sis.feature.DefaultAttributeType#getDefaultValue() default value}.</p>
+      *
+      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} should be
+      * {@link javax.measure.Unit}.</p>
+      */
+     public static final ScopedName UNIT_CHARACTERISTIC = Names.createScopedName(SCOPE, null, "unit");
+ 
+     /**
+      * Conventional name for fetching the maximal length of string values.
+      * The maximal length is stored as the
+      * {@linkplain org.apache.sis.feature.DefaultAttributeType#getDefaultValue() default value} of the
+      * {@linkplain org.apache.sis.feature.DefaultAttributeType#characteristics() characteristic} associated
+      * to the attribute on which the maximal length applies.
+      *
+      * <p>The {@linkplain org.apache.sis.feature.DefaultAttributeType#getValueClass() value class} should be
+      * {@link Integer}.</p>
+      *
 -     * @see #getMaximalLengthCharacteristic(Property)
++     * @see #getMaximalLengthCharacteristic(Object)
+      */
+     public static final ScopedName MAXIMAL_LENGTH_CHARACTERISTIC = Names.createScopedName(SCOPE, null, "maximalLength");
+ 
+     /**
+      * Conventional name for fetching the enumeration of valid values.
+      * The set of valid values is stored stored as the
+      * {@linkplain org.apache.sis.feature.DefaultAttributeType#getDefaultValue() default value} of the
+      * {@linkplain org.apache.sis.feature.DefaultAttributeType#characteristics() characteristic} associated
+      * to the attribute on which the restriction applies.
+      */
+     public static final GenericName VALID_VALUES_CHARACTERISTIC = Names.createScopedName(SCOPE, null, "validValues");
+ 
+     /**
+      * String representation of the {@link #IDENTIFIER_PROPERTY} name.
 -     * This can be used in calls to {@link Feature#getPropertyValue(String)}.
++     * This can be used in calls to {@link AbstractFeature#getPropertyValue(String)}.
+      */
+     public static final String IDENTIFIER = "sis:identifier";
+ 
+     /**
+      * String representation of the {@link #GEOMETRY_PROPERTY} name.
 -     * This can be used in calls to {@link Feature#getPropertyValue(String)}.
++     * This can be used in calls to {@link AbstractFeature#getPropertyValue(String)}.
+      */
+     public static final String GEOMETRY = "sis:geometry";
+ 
+     /**
+      * String representation of the {@link #ENVELOPE_PROPERTY} name.
+      * This can be used in calls to {@link Feature#getPropertyValue(String)}.
+      */
+     public static final String ENVELOPE = "sis:envelope";
+ 
+     /**
+      * String representation of the {@link #CRS_CHARACTERISTIC} name.
+      */
+     public static final String CRS = "sis:crs";
+ 
+     /**
+      * String representation of the {@link #UNIT_CHARACTERISTIC} name.
+      */
+     public static final String UNIT = "sis:unit";
+ 
+     /**
+      * String representation of the {@link #MAXIMAL_LENGTH_CHARACTERISTIC} name.
+      */
+     public static final String MAXIMAL_LENGTH = "sis:maximalLength";
+ 
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private AttributeConvention() {
+     }
+ 
+     /**
+      * Returns {@code true} if the given name stands for one of the synthetic properties defined by convention.
+      * Conventional properties are properties added by the {@code DataStore} to the {@code FeatureType} in order
+      * to provide a uniform way to access commonly used information.
+      *
+      * <p>Synthetic properties should generally not be written by the user.
+      * Those properties are calculated most of the time and have only a meaning within SIS.</p>
+      *
+      * <p>Current implementation returns {@code true} if the given name is in the SIS namespace.</p>
+      *
+      * @param  name  the name of the property or characteristic to test, or {@code null}.
+      * @return {@code true} if the given name is non-null and in the SIS namespace.
+      */
+     public static boolean contains(GenericName name) {
+         while (name instanceof ScopedName) {
+             if (SCOPE.equals(((ScopedName) name).path())) {
+                 return true;
+             }
+             name = ((ScopedName) name).tail();
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns {@code true} if the given feature type is non-null and has a {@value #IDENTIFIER} property.
+      *
+      * @param  feature  the feature type to test, or {@code null}.
+      * @return whether the given feature type is non-null and has a {@value #IDENTIFIER} property.
+      */
 -    public static boolean hasIdentifier(final FeatureType feature) {
++    public static boolean hasIdentifier(final DefaultFeatureType feature) {
+         if (feature != null) try {
+             return feature.getProperty(IDENTIFIER) != null;
 -        } catch (PropertyNotFoundException e) {
++        } catch (IllegalArgumentException e) {
+             // Ignore
+         }
+         return false;
+     }
+ 
+     /**
 -     * Returns {@code true} if the given type is an {@link AttributeType} or an {@link Operation} computing
++     * Returns {@code true} if the given type is an {@code AttributeType} or an {@code Operation} computing
+      * an attribute, and the attribute value is one of the geometry types recognized by SIS.
+      * The types currently recognized by SIS are:
+      *
+      * <ul>
+      *   <li>{@link com.esri.core.geometry.Geometry} of the ESRI's API.</li>
+      * </ul>
+      *
+      * The above list may be expanded in any future SIS version.
+      *
+      * @param  type  the type to test, or {@code null}.
+      * @return {@code true} if the given type is (directly or indirectly) an attribute type
+      *         for one of the recognized geometry types.
+      *
+      * @see #GEOMETRY_PROPERTY
+      */
 -    public static boolean isGeometryAttribute(final IdentifiedType type) {
 -        final Optional<AttributeType<?>> at = Features.toAttribute(type);
++    public static boolean isGeometryAttribute(final AbstractIdentifiedType type) {
++        final Optional<DefaultAttributeType<?>> at = Features.toAttribute(type);
+         return at.isPresent() && Geometries.isKnownType(at.get().getValueClass());
+     }
+ 
+     /**
+      * Returns whether the given operation or attribute type is characterized by a coordinate reference system.
+      * This method verifies whether a characteristic named {@link #CRS_CHARACTERISTIC} with values assignable to
+      * {@link CoordinateReferenceSystem} exists (directly or indirectly) for the given type.
+      *
+      * @param  type  the operation or attribute type for which to get the CRS, or {@code null}.
+      * @return {@code true} if a characteristic for Coordinate Reference System has been found.
+      */
 -    public static boolean characterizedByCRS(final IdentifiedType type) {
++    public static boolean characterizedByCRS(final AbstractIdentifiedType type) {
+         return hasCharacteristic(type, CRS, CoordinateReferenceSystem.class);
+     }
+ 
+     /**
+      * Returns the Coordinate Reference Systems characteristic for the given attribute, or {@code null} if none.
+      * This method gets the value or default value from the characteristic named {@link #CRS_CHARACTERISTIC}.
+      *
+      * @param  attribute  the attribute for which to get the CRS, or {@code null}.
+      * @return the Coordinate Reference System characteristic of the given attribute, or {@code null} if none.
+      * @throws ClassCastException if {@link #CRS_CHARACTERISTIC} has been found but is associated
+      *         to an object which is not a {@link CoordinateReferenceSystem} instance.
+      *
+      * @see org.apache.sis.feature.builder.AttributeTypeBuilder#setCRS(CoordinateReferenceSystem)
+      */
 -    public static CoordinateReferenceSystem getCRSCharacteristic(final Property attribute) {
++    public static CoordinateReferenceSystem getCRSCharacteristic(final Object attribute) {
+         return (CoordinateReferenceSystem) getCharacteristic(attribute, CRS);
+     }
+ 
+     /**
+      * Returns the Coordinate Reference Systems characteristic for the given property type, or {@code null} if none.
+      * This method gets the default value from the characteristic named {@link #CRS_CHARACTERISTIC}.
+      * If the given property is a link, then this method follows the link in the given feature type (if non-null).
+      *
+      * <p>This method should be used only when the actual property instance is unknown.
 -     * Otherwise, {@link #getCRSCharacteristic(Property)} should be used because the CRS
++     * Otherwise, {@code getCRSCharacteristic(Property)} should be used because the CRS
+      * may vary for each property instance.</p>
+      *
+      * @param  feature    the feature type in which to follow links, or {@code null} if none.
+      * @param  attribute  the attribute type for which to get the CRS, or {@code null}.
+      * @return the Coordinate Reference System characteristic of the given property type, or {@code null} if none.
+      * @throws ClassCastException if {@link #CRS_CHARACTERISTIC} has been found but is associated
+      *         to an object which is not a {@link CoordinateReferenceSystem} instance.
+      */
 -    public static CoordinateReferenceSystem getCRSCharacteristic(final FeatureType feature, final PropertyType attribute) {
++    public static CoordinateReferenceSystem getCRSCharacteristic(final DefaultFeatureType feature, final AbstractIdentifiedType attribute) {
+         return (CoordinateReferenceSystem) getCharacteristic(feature, attribute, CRS);
+     }
+ 
+     /**
+      * Returns whether the given operation or attribute type is characterized by a maximal length.
+      * This method verifies whether a characteristic named {@link #MAXIMAL_LENGTH_CHARACTERISTIC}
+      * with values of class {@link Integer} exists (directly or indirectly) for the given type.
+      *
+      * @param  type  the operation or attribute type for which to get the maximal length, or {@code null}.
+      * @return {@code true} if a characteristic for maximal length has been found.
+      */
 -    public static boolean characterizedByMaximalLength(final IdentifiedType type) {
++    public static boolean characterizedByMaximalLength(final AbstractIdentifiedType type) {
+         return hasCharacteristic(type, MAXIMAL_LENGTH, Integer.class);
+     }
+ 
+     /**
+      * Returns the maximal length characteristic for the given attribute, or {@code null} if none.
+      * This method gets the value or default value from the characteristic named {@link #MAXIMAL_LENGTH_CHARACTERISTIC}.
+      *
+      * @param  attribute  the attribute for which to get the maximal length, or {@code null}.
+      * @return the maximal length characteristic of the given attribute, or {@code null} if none.
+      * @throws ClassCastException if {@link #MAXIMAL_LENGTH_CHARACTERISTIC} has been found but is associated
+      *         to an object which is not an {@link Integer} instance.
+      *
+      * @see org.apache.sis.feature.builder.AttributeTypeBuilder#setMaximalLength(Integer)
+      */
 -    public static Integer getMaximalLengthCharacteristic(final Property attribute) {
++    public static Integer getMaximalLengthCharacteristic(final Object attribute) {
+         return (Integer) getCharacteristic(attribute, MAXIMAL_LENGTH);
+     }
+ 
+     /**
+      * Returns the maximal length characteristic for the given property type, or {@code null} if none.
+      * This method gets the default value from the characteristic named {@link #MAXIMAL_LENGTH_CHARACTERISTIC}.
+      * If the given property is a link, then this method follows the link in the given feature type (if non-null).
+      *
+      * <p>This method should be used only when the actual property instance is unknown.
 -     * Otherwise, {@link #getMaximalLengthCharacteristic(Property)} should be used because
++     * Otherwise, {@code getMaximalLengthCharacteristic(Property)} should be used because
+      * the maximal length may vary for each property instance.</p>
+      *
+      * @param  feature    the feature type in which to follow links, or {@code null} if none.
+      * @param  attribute  the attribute type for which to get the maximal length, or {@code null}.
+      * @return the maximal length characteristic of the given property type, or {@code null} if none.
+      * @throws ClassCastException if {@link #MAXIMAL_LENGTH_CHARACTERISTIC} has been found but is associated
+      *         to an object which is not a {@link CoordinateReferenceSystem} instance.
+      */
 -    public static Integer getMaximalLengthCharacteristic(final FeatureType feature, final PropertyType attribute) {
++    public static Integer getMaximalLengthCharacteristic(final DefaultFeatureType feature, final AbstractIdentifiedType attribute) {
+         return (Integer) getCharacteristic(feature, attribute, MAXIMAL_LENGTH);
+     }
+ 
+     /**
+      * Returns {@code true} if the given operation or attribute type has a characteristic of the given name,
+      * and the values of that characteristic are assignable to the given {@code valueClass}.
+      *
+      * @param  type        the operation or attribute type for which to test the existence of a characteristic.
+      * @param  name        the name of the characteristic to test.
+      * @param  valueClass  the expected characteristic values.
+      * @return {@code true} if a characteristic of the given name exists and has values assignable to the given class.
+      */
 -    private static boolean hasCharacteristic(IdentifiedType type, final String name, final Class<?> valueClass) {
 -        final Optional<AttributeType<?>> at = Features.toAttribute(type);
++    private static boolean hasCharacteristic(AbstractIdentifiedType type, final String name, final Class<?> valueClass) {
++        final Optional<DefaultAttributeType<?>> at = Features.toAttribute(type);
+         if (at.isPresent()) {
 -            final AttributeType<?> ct = at.get().characteristics().get(name);
++            final DefaultAttributeType<?> ct = at.get().characteristics().get(name);
+             if (ct != null) {
+                 return valueClass.isAssignableFrom(ct.getValueClass());
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Fetches from the given property the value or default value of the named characteristic.
+      * If the given property is null, or is not an attribute, or does not have characteristics
+      * of the given name, then this method returns {@code null}.
+      *
+      * @param  attribute  the attribute from which to get the characteristic value or default value, or {@code null}.
+      * @param  name       name of the characteristic to get.
+      * @return the value or default value of the given characteristic in the given property, or {@code null} if none.
+      */
 -    private static Object getCharacteristic(final Property attribute, final String name) {
 -        if (attribute instanceof Attribute<?>) {
 -            final Attribute<?> at = ((Attribute<?>) attribute).characteristics().get(name);
++    private static Object getCharacteristic(final Object attribute, final String name) {
++        if (attribute instanceof AbstractAttribute<?>) {
++            final AbstractAttribute<?> at = ((AbstractAttribute<?>) attribute).characteristics().get(name);
+             if (at != null) {
+                 final Object value = at.getValue();
+                 if (value != null) {
+                     return value;
+                 }
+             }
 -            final AttributeType<?> type = ((Attribute<?>) attribute).getType().characteristics().get(name);
++            final DefaultAttributeType<?> type = ((AbstractAttribute<?>) attribute).getType().characteristics().get(name);
+             if (type != null) {
+                 return type.getDefaultValue();
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Fetches from the given property the default value of the characteristic of the given name.
+      * If the given property is a link, then this method follows the link in the given feature type
+      * (unless that feature type is null).
+      *
+      * @param  feature         the feature type in which to follow links, or {@code null} if none.
+      * @param  property        the property from which to get the characteristic value, or {@code null}.
+      * @param  characteristic  name of the characteristic from which to get the default value.
+      * @return the default value of the named characteristic in the given property, or {@code null} if none.
+      */
 -    private static Object getCharacteristic(final FeatureType feature, PropertyType property, final String characteristic) {
++    private static Object getCharacteristic(final DefaultFeatureType feature, AbstractIdentifiedType property, final String characteristic) {
+         final Optional<String> referent = Features.getLinkTarget(property);
+         if (referent.isPresent() && feature != null) {
+             property = feature.getProperty(referent.get());
+         }
 -        if (property instanceof AttributeType<?>) {
 -            final AttributeType<?> type = ((AttributeType<?>) property).characteristics().get(characteristic);
++        if (property instanceof DefaultAttributeType<?>) {
++            final DefaultAttributeType<?> type = ((DefaultAttributeType<?>) property).characteristics().get(characteristic);
+             if (type != null) {
+                 return type.getDefaultValue();
+             }
+         }
+         return null;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
index 0000000000,b60a74b5e9..9c1b95725a
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
@@@ -1,0 -1,128 +1,127 @@@
+ /*
+  * 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.feature.privy;
+ 
+ import java.util.Set;
+ import org.apache.sis.math.FunctionProperty;
+ import org.apache.sis.filter.Optimization;
+ import org.apache.sis.filter.DefaultFilterFactory;
+ import org.apache.sis.feature.builder.FeatureTypeBuilder;
+ import org.apache.sis.feature.builder.PropertyTypeBuilder;
+ import org.apache.sis.feature.builder.AttributeTypeBuilder;
+ import org.apache.sis.filter.internal.Node;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.filter.Expression;
++import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.pending.geoapi.filter.Literal;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * OGC expressions or other functions operating on feature instances.
+  * This interface adds an additional method, {@link #expectedType(FeatureType, FeatureTypeBuilder)},
+  * for fetching in advance the expected type of expression results.
+  *
+  * <p>This is an experimental interface which may be removed in any future version.</p>
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
++ * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
+  * @param  <V>  the type of values computed by the expression.
+  */
+ public interface FeatureExpression<R,V> extends Expression<R,V> {
+     /**
+      * Returns the manner in which values are computed from given resources.
+      * The default implementation combines the properties of all parameters.
+      *
+      * @return the manners in which values are computed from resources.
+      */
+     default Set<FunctionProperty> properties() {
+         return Node.transitiveProperties(getParameters());
+     }
+ 
+     /**
+      * Returns the type of values computed by this expression, or {@code Object.class} if unknown.
+      *
+      * <h4>Note on type safety</h4>
+      * The parameterized type should be {@code <? extends V>} because some implementations get this
+      * information by a call to {@code value.getClass()}. But it should also be {@code <? super V>}
+      * for supporting the {@code Object.class} return value. Those contradictory requirements force
+      * us to use {@code <?>}.
+      *
+      * @return the type of values computed by this expression.
+      */
+     default Class<?> getValueClass() {
+         return Object.class;
+     }
+ 
+     /**
+      * Provides the expected type of values produced by this expression when a feature of the given
 -     * type is evaluated. The resulting type shall describe a "static" property, i.e. it can be an
 -     * {@link AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
 -     * but not an {@link org.opengis.feature.Operation}.
++     * type is evaluated. The resulting type shall describe a "static" property, i.e. it can be a
++     * {@link org.apache.sis.feature.DefaultAttributeType} or a {@link org.apache.sis.feature.DefaultAssociationRole}
++     * but not an {@link org.apache.sis.feature.AbstractOperation}.
+      *
+      * <p>If this method returns an instance of {@link AttributeTypeBuilder}, then its parameterized
+      * type should be the same {@code <V>} than this {@code FeatureExpression}.</p>
+      *
+      * @param  valueType  the type of features to be evaluated by the given expression.
+      * @param  addTo      where to add the type of properties evaluated by this expression.
+      * @return builder of the added property, or {@code null} if this method cannot add a property.
+      * @throws IllegalArgumentException if this method can operate only on some feature types
+      *         and the given type is not one of them.
+      */
 -    PropertyTypeBuilder expectedType(FeatureType valueType, FeatureTypeBuilder addTo);
++    PropertyTypeBuilder expectedType(DefaultFeatureType valueType, FeatureTypeBuilder addTo);
+ 
+     /**
+      * Tries to cast or convert the given expression to a {@link FeatureExpression}.
+      * If the given expression cannot be casted, then this method creates a copy
+      * provided that the expression is one of the following type:
+      *
+      * <ol>
+      *   <li>{@link Literal}.</li>
+      *   <li>{@link ValueReference}, assuming that the expression expects feature instances.</li>
+      * </ol>
+      *
+      * Otherwise this method returns {@code null}.
+      * It is caller's responsibility to verify if this method returns {@code null} and to throw an exception in such case.
+      * We leave that responsibility to the caller because (s)he may be able to provide better error messages.
+      *
+      * <h4>Note on type safety</h4>
+      * This method does not use parameterized types because of the assumption on {@link ValueReference}.
+      * As of Apache SIS 1.3, we have no way to check if {@code <R>} is for feature instances.
+      *
+      * @param  candidate  the expression to cast or copy. Can be null.
+      * @return the given expression as a feature expression, or {@code null} if it cannot be casted or converted.
+      */
+     public static FeatureExpression<?,?> castOrCopy(final Expression<?,?> candidate) {
+         if (candidate instanceof FeatureExpression<?,?>) {
+             return (FeatureExpression<?,?>) candidate;
+         }
+         final Expression<?,?> copy;
+         if (candidate instanceof Literal<?,?>) {
+             copy = Optimization.literal(((Literal<?,?>) candidate).getValue());
+         } else if (candidate instanceof ValueReference<?,?>) {
+             final String xpath = ((ValueReference<?,?>) candidate).getXPath();
+             copy = DefaultFilterFactory.forFeatures().property(xpath);
+         } else {
+             return null;
+         }
+         return (FeatureExpression<?,?>) copy;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureUtilities.java
index 0000000000,b089754d1a..50aca20234
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureUtilities.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureUtilities.java
@@@ -1,0 -1,92 +1,92 @@@
+ /*
+  * 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.feature.privy;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.Iterator;
+ import java.util.Collection;
+ import java.util.ConcurrentModificationException;
+ import org.opengis.util.GenericName;
+ import org.opengis.metadata.Identifier;
+ import org.opengis.parameter.ParameterDescriptor;
+ import org.opengis.parameter.ParameterDescriptorGroup;
+ import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.util.Static;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.PropertyType;
++// Specific to the main branch:
++import org.apache.sis.feature.AbstractIdentifiedType;
+ 
+ 
+ /**
+  * Non-public utility methods for Apache SIS internal usage.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class FeatureUtilities extends Static {
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private FeatureUtilities() {
+     }
+ 
+     /**
+      * Creates a parameter descriptor in the Apache SIS namespace. This convenience method shall
+      * not be in public API, because users should define operations in their own namespace.
+      *
+      * @param  name           the parameter group name, typically the same as operation name.
+      * @param  parameters     the parameters, or an empty array if none.
+      * @return description of the parameters group.
+      */
+     public static ParameterDescriptorGroup parameters(final String name, final ParameterDescriptor<?>... parameters) {
+         final Map<String,Object> properties = new HashMap<>(4);
+         properties.put(ParameterDescriptorGroup.NAME_KEY, name);
+         properties.put(Identifier.AUTHORITY_KEY, Citations.SIS);
+         return new DefaultParameterDescriptorGroup(properties, 1, 1);
+     }
+ 
+     /**
+      * Gets the name of all given properties. If any property is null or has a null name,
+      * then the corresponding entry in the returned array will be null.
+      *
+      * @param  properties  the properties for which to get the names, or {@code null}.
+      * @return the name of all given properties, or {@code null} if the given list was null.
+      */
 -    public static String[] getNames(final Collection<? extends PropertyType> properties) {
++    public static String[] getNames(final Collection<? extends AbstractIdentifiedType> properties) {
+         if (properties == null) {
+             return null;
+         }
+         final String[] names = new String[properties.size()];
 -        final Iterator<? extends PropertyType> it = properties.iterator();
++        final Iterator<? extends AbstractIdentifiedType> it = properties.iterator();
+         for (int i=0; i < names.length; i++) {
 -            final PropertyType property = it.next();
++            final AbstractIdentifiedType property = it.next();
+             if (property != null) {
+                 final GenericName name = property.getName();
+                 if (name != null) {
+                     names[i] = name.toString();
+                 }
+             }
+         }
+         // Should not have any element left, unless collection size changed during iteration.
+         if (it.hasNext()) {
+             throw new ConcurrentModificationException();
+         }
+         return names;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/MovingFeatures.java
index 0000000000,c468b4d0dd..5a78fafad0
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/MovingFeatures.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/MovingFeatures.java
@@@ -1,0 -1,130 +1,129 @@@
+ /*
+  * 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.feature.privy;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.time.Instant;
+ import org.apache.sis.util.iso.Names;
+ import org.apache.sis.math.Vector;
+ import org.apache.sis.feature.DefaultAttributeType;
+ import org.apache.sis.referencing.crs.DefaultTemporalCRS;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.AttributeType;
++// Specific to the main branch:
++import org.apache.sis.feature.AbstractAttribute;
+ 
+ 
+ /**
+  * Utility methods for instantiating features where the geometry is a trajectory
+  * and some property values may change with time.
+  *
+  * This class is <strong>not</strong> thread-safe.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public class MovingFeatures {
+     /**
+      * Definition of characteristics containing a list of instants, without duplicates.
+      * Should be in chronological order, but this is not verified.
+      */
 -    public static final AttributeType<Instant> TIME_AS_INSTANTS;
++    public static final DefaultAttributeType<Instant> TIME_AS_INSTANTS;
+ 
+     /**
+      * An alternative to {@link #TIME_AS_INSTANTS} used when times cannot be mapped to calendar dates.
+      * This characteristic uses the same name as {@code TIME_AS_INSTANTS}. Consequently, at most one
+      * of {@code TIME_AS_INSTANTS} and {@code TIME_AS_NUMBERS} can be used on the same property.
+      */
 -    private static final AttributeType<Number> TIME_AS_NUMBERS;
++    private static final DefaultAttributeType<Number> TIME_AS_NUMBERS;
+     static {
+         final var scope = Names.createLocalName("OGC", null, "MF");
+         final var properties = Map.of(DefaultAttributeType.NAME_KEY, Names.createScopedName(scope, null, "datetimes"));
+         TIME_AS_INSTANTS = new DefaultAttributeType<>(properties, Instant.class, 0, Integer.MAX_VALUE, null);
+         TIME_AS_NUMBERS  = new DefaultAttributeType<>(properties,  Number.class, 0, Integer.MAX_VALUE, null);
+     }
+ 
+     /**
+      * Returns the "datetimes" characteristic to add on an attribute type.
+      * The characteristic will expect either {@link Instant} or {@link Number} values,
+      * depending on whether a temporal CRS is available or not.
+      *
+      * @param  hasCRS  whether a temporal CRS is available.
+      * @return the "datetimes" characteristic.
+      */
 -    public static AttributeType<?> characteristic(final boolean hasCRS) {
++    public static DefaultAttributeType<?> characteristic(final boolean hasCRS) {
+         return hasCRS ? TIME_AS_INSTANTS : TIME_AS_NUMBERS;
+     }
+ 
+     /**
+      * Caches of list of times or instants, used for sharing existing instances.
+      * We do this sharing because it is common to have many properties having the same time characteristics.
+      */
+     private final Map<Vector,InstantList> cache;
+ 
+     /**
+      * Creates a new builder.
+      *
+      * @param  share  other builder that may share time vectors, or {@code null} if none.
+      */
+     public MovingFeatures(final MovingFeatures share) {
+         cache = (share != null) ? share.cache : new HashMap<>();
+     }
+ 
+     /**
+      * Sets the "datetimes" characteristic on the given attribute as a list of {@link Instant} instances.
+      * Should be in chronological order, but this is not verified.
+      *
+      * @param  dest    the attribute on which to set time characteristic.
+      * @param  millis  times in milliseconds since the epoch.
+      */
 -    public final void setInstants(final Attribute<?> dest, final long[] millis) {
 -        final Attribute<Instant> c = TIME_AS_INSTANTS.newInstance();
++    public final void setInstants(final AbstractAttribute<?> dest, final long[] millis) {
++        final AbstractAttribute<Instant> c = TIME_AS_INSTANTS.newInstance();
+         c.setValues(cache.computeIfAbsent(InstantList.vectorize(millis), InstantList::new));
+         dest.characteristics().values().add(c);
+     }
+ 
+     /**
+      * Sets the "datetimes" characteristic on the given attribute.
+      * If the {@code converter} is non-null, it will be used for converting values to {@link Instant} instances.
+      * Otherwise values are stored as-is as time elapsed in arbitrary units since an arbitrary epoch.
+      *
+      * <p>Values should be in chronological order, but this is not verified.
+      * Current implementation does not cache the values, but this policy may be revisited in a future version.</p>
+      *
+      * @param  dest       the attribute on which to set time characteristic.
+      * @param  values     times in arbitrary units since an arbitrary epoch.
+      * @param  converter  the CRS to use for converting values to {@link Instant} instances, or {@code null}.
+      */
 -    public static void setTimes(final Attribute<?> dest, final Vector values, final DefaultTemporalCRS converter) {
 -        final Attribute<?> ct;
++    public static void setTimes(final AbstractAttribute<?> dest, final Vector values, final DefaultTemporalCRS converter) {
++        final AbstractAttribute<?> ct;
+         if (converter != null) {
+             final Instant[] instants = new Instant[values.size()];
+             for (int i=0; i<instants.length; i++) {
+                 instants[i] = converter.toInstant(values.doubleValue(i));
+             }
 -            final Attribute<Instant> c = TIME_AS_INSTANTS.newInstance();
++            final AbstractAttribute<Instant> c = TIME_AS_INSTANTS.newInstance();
+             c.setValues(UnmodifiableArrayList.wrap(instants));
+             ct = c;
+         } else {
 -            final Attribute<Number> c = TIME_AS_NUMBERS.newInstance();
++            final AbstractAttribute<Number> c = TIME_AS_NUMBERS.newInstance();
+             c.setValues(values);
+             ct = c;
+         }
+         dest.characteristics().values().add(ct);
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index 15620e9101,f35237d4d7..42def4b9b3
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@@ -25,13 -25,19 +25,13 @@@ import org.opengis.referencing.operatio
  import org.apache.sis.geometry.wrapper.Geometries;
  import org.apache.sis.geometry.wrapper.GeometryWrapper;
  import org.apache.sis.geometry.wrapper.SpatialOperationContext;
- import org.apache.sis.feature.internal.AttributeConvention;
+ import org.apache.sis.feature.privy.AttributeConvention;
  import org.apache.sis.filter.internal.Node;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.SpatialOperator;
 -import org.opengis.filter.BinarySpatialOperator;
 -import org.opengis.filter.InvalidFilterValueException;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyNotFoundException;
 +// Specific to the main branch:
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/FunctionRegister.java
index 0000000000,1789b46fb0..810ede286f
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/FunctionRegister.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/FunctionRegister.java
@@@ -1,0 -1,78 +1,63 @@@
+ /*
+  * 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.filter;
+ 
+ import java.util.Collection;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.capability.AvailableFunction;
 -
+ 
+ /**
+  * A factory of {@code org.opengis.filter} functions identified by their names.
+  * Each factory can provide an arbitrary number of functions, enumerated by {@link #getNames()}.
+  * The {@link org.apache.sis.filter.DefaultFilterFactory#function(String, Expression...)} method
+  * delegates to this interface for creating the function implementation for a given name.
+  *
+  * <p><b>Warning:</b> there is currently no mechanism for avoiding name collision.
+  * It is implementer responsibility to keep trace of the whole universe of functions and avoid collision.
+  * Checks against name collisions may be added in a future version.</p>
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
 - *
 - * @see org.opengis.filter.FilterFactory#function(String, Expression...)
+  */
+ public interface FunctionRegister {
+     /**
+      * Returns the name of the standard or authority defining the functions.
+      *
+      * @return provider of function definitions.
+      */
+     String getAuthority();
+ 
+     /**
+      * Returns the names of all functions that this factory can create.
+      * It is currently implementer responsibility to ensure that there are no name collisions with
+      * functions provided by other factories (this problem may be improved in future SIS release).
+      *
+      * @return set of supported function names.
+      */
+     Collection<String> getNames();
+ 
 -    /**
 -     * Describes the parameters of a function.
 -     *
 -     * @param  name  name of the function to describe (not null).
 -     * @return description of the function parameters.
 -     * @throws IllegalArgumentException if function name is unknown..
 -     */
 -    AvailableFunction describe(String name) throws IllegalArgumentException;
 -
+     /**
+      * Creates a new function of the given name with given parameters.
+      *
 -     * @param  <R>         the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
++     * @param  <R>         the type of resources (e.g. {@code Feature}) used as inputs.
+      * @param  name        name of the function to create (not null).
+      * @param  parameters  function parameters.
+      * @return function for the given name and parameters.
+      * @throws IllegalArgumentException if function name is unknown or some parameters are illegal.
+      */
+     <R> Expression<R,?> create(String name, Expression<R,?>[] parameters) throws IllegalArgumentException;
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
index 348936ea2e,54bf782cf1..4d3919e7a1
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
@@@ -20,10 -20,13 +20,10 @@@ import java.util.List
  import java.util.Collection;
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.filter.internal.Node;
- import org.apache.sis.feature.internal.AttributeConvention;
+ import org.apache.sis.feature.privy.AttributeConvention;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ResourceId;
 -import org.opengis.filter.Filter;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
index dde264f0d5,42ef29a5cb..6127180fbf
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LogicalFilter.java
@@@ -22,12 -22,13 +22,12 @@@ import java.util.LinkedHashSet
  import java.util.Objects;
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.filter.internal.Node;
- import org.apache.sis.util.internal.CollectionsExt;
- import org.apache.sis.util.internal.UnmodifiableArrayList;
+ import org.apache.sis.util.privy.CollectionsExt;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.LogicalOperatorName;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.filter.LogicalOperator;
 +import org.apache.sis.pending.geoapi.filter.LogicalOperatorName;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
index 1392b878e3,5a396dc0b4..40c624b6a4
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
@@@ -27,13 -27,15 +27,13 @@@ import org.opengis.util.CodeList
  import org.apache.sis.math.FunctionProperty;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.filter.internal.Node;
- import org.apache.sis.util.internal.CollectionsExt;
+ import org.apache.sis.util.privy.CollectionsExt;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.LogicalOperatorName;
 -import org.opengis.feature.FeatureType;
 +// Specific to the main branch:
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.LogicalOperator;
 +import org.apache.sis.pending.geoapi.filter.LogicalOperatorName;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index 1b8a87fd08,bb62d7559c..9fc8e48551
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
@@@ -26,18 -26,18 +26,18 @@@ import org.apache.sis.util.Unconvertibl
  import org.apache.sis.feature.builder.FeatureTypeBuilder;
  import org.apache.sis.feature.builder.PropertyTypeBuilder;
  import org.apache.sis.feature.builder.AttributeTypeBuilder;
- import org.apache.sis.filter.internal.XPath;
+ import org.apache.sis.filter.privy.XPath;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.opengis.util.ScopedName;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.AbstractIdentifiedType;
 +import org.apache.sis.feature.AbstractOperation;
 +import org.apache.sis.feature.DefaultAttributeType;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Name;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/FunctionNames.java
index 0000000000,06be9b3463..ad709c9195
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/FunctionNames.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/FunctionNames.java
@@@ -1,0 -1,89 +1,89 @@@
+ /*
+  * 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.filter.privy;
+ 
+ import org.apache.sis.util.Static;
+ import org.apache.sis.filter.sqlmm.SQLMM;
+ 
+ 
+ /**
+  * Names of some expressions used in Apache SIS.
+  * This class defines only the names that need to be referenced from at least two different classes.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class FunctionNames extends Static {
 -    /** Value of {@link org.opengis.filter.NullOperator#getOperatorType()}. */
++    /** Value of {@code NullOperator.getOperatorType()}. */
+     public static final String PROPERTY_IS_NULL = "PROPERTY_IS_NULL";
+ 
 -    /** Value of {@link org.opengis.filter.NilOperator#getOperatorType()}. */
++    /** Value of {@code NilOperator.getOperatorType()}. */
+     public static final String PROPERTY_IS_NIL = "PROPERTY_IS_NIL";
+ 
 -    /** Value of {@link org.opengis.filter.LikeOperator#getOperatorType()}. */
++    /** Value of {@code LikeOperator.getOperatorType()}. */
+     public static final String PROPERTY_IS_LIKE = "PROPERTY_IS_LIKE";
+ 
+     /** Value of {@link org.opengis.filter.BetweenComparisonOperator#getOperatorType()}. */
+     public static final String PROPERTY_IS_BETWEEN = "PROPERTY_IS_BETWEEN";
+ 
 -    /** Value of {@link org.opengis.filter.Literal#getFunctionName()}. */
++    /** Value of {@code Literal.getFunctionName()}. */
+     public static final String Literal = "Literal";
+ 
 -    /** Value of {@link org.opengis.filter.ValueReference#getFunctionName()}. */
++    /** Value of {@code ValueReference.getFunctionName()}. */
+     public static final String ValueReference = "ValueReference";
+ 
+     /** The "Add" (+) arithmetic expression. */
+     public static final String Add = "Add";
+ 
+     /** The "Subtract" (−) arithmetic expression. */
+     public static final String Subtract = "Subtract";
+ 
+     /** The "Multiply" (×) arithmetic expression. */
+     public static final String Multiply = "Multiply";
+ 
+     /** The "Divide" (÷) arithmetic expression. */
+     public static final String Divide = "Divide";
+ 
+     /** Name of {@link SQLMM#ST_Contains}. */
+     public static final String ST_Contains = "ST_Contains";
+ 
+     /** Name of {@link SQLMM#ST_Crosses}. */
+     public static final String ST_Crosses = "ST_Crosses";
+ 
+     /** Name of {@link SQLMM#ST_Disjoint}. */
+     public static final String ST_Disjoint = "ST_Disjoint";
+ 
+     /** Name of {@link SQLMM#ST_Equals}. */
+     public static final String ST_Equals = "ST_Equals";
+ 
+     /** Name of {@link SQLMM#ST_Intersects}. */
+     public static final String ST_Intersects = "ST_Intersects";
+ 
+     /** Name of {@link SQLMM#ST_Overlaps}. */
+     public static final String ST_Overlaps = "ST_Overlaps";
+ 
+     /** Name of {@link SQLMM#ST_Touches}. */
+     public static final String ST_Touches = "ST_Touches";
+ 
+     /** Name of {@link SQLMM#ST_Within}. */
+     public static final String ST_Within = "ST_Within";
+ 
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private FunctionNames() {
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/SortByComparator.java
index 0000000000,624e60d7c2..9c1d3a5db4
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/SortByComparator.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/SortByComparator.java
@@@ -1,0 -1,203 +1,203 @@@
+ /*
+  * 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.filter.privy;
+ 
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.Comparator;
+ import java.io.Serializable;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.SortBy;
 -import org.opengis.filter.SortProperty;
 -import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.pending.geoapi.filter.SortBy;
++import org.apache.sis.pending.geoapi.filter.SortProperty;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * Comparator sorting features using an array of {@link SortProperty} elements applied in order.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  *
+  * @param  <R>  the type of resources (typically {@code Feature}) to sort.
+  */
+ public final class SortByComparator<R> implements SortBy<R>, Serializable {
+     /**
+      * For cross-version compatibility.
+      */
+     private static final long serialVersionUID = -7964849249532212389L;
+ 
+     /**
+      * The sort order specified to the constructor.
+      *
+      * @see #getSortProperties()
+      */
+     @SuppressWarnings("serial")                     // Most SIS implementations are serializable.
+     private final SortProperty<R>[] properties;
+ 
+     /**
+      * Creates a new comparator for the given sort expression.
+      * This is shortcut for the common case where there is a single expression.
+      *
+      * @param  p  the property to wrap in a {@link SortBy} comparator.
+      */
+     @SuppressWarnings({"unchecked","rawtypes"})             // Generic array creation.
+     private SortByComparator(final SortProperty<R> p) {
+         ArgumentChecks.ensureNonNullElement("properties", 0, p);
+         properties = new SortProperty[] {p};
+     }
+ 
+     /**
+      * Creates a new comparator with the values of given map.
+      * The map is used for removing duplicated expressions.
+      */
+     @SuppressWarnings({"unchecked","rawtypes"})             // Generic array creation.
+     private SortByComparator(final Map<?, SortProperty<R>> merged) {
+         properties = merged.values().toArray(SortProperty[]::new);
+     }
+ 
+     /**
+      * Creates a new comparator for the given sort expressions.
+      *
+      * @param  <R>         the type of resources (typically {@code Feature}) to sort.
+      * @param  properties  the sort order.
+      * @return the comparator, or {@code null} if the given array is empty.
+      */
+     public static <R> SortByComparator<R> create(final SortProperty<R>[] properties) {
+         switch (properties.length) {
+             case 0: return null;
+             case 1: return new SortByComparator<>(properties[0]);
+         }
+         final Map<ValueReference<R,?>, SortProperty<R>> merged = new LinkedHashMap<>();
+         addAll(Arrays.asList(properties), merged);
+         return new SortByComparator<>(merged);
+     }
+ 
+     /**
+      * Creates a new comparator as the concatenation of the two given comparators.
+      * The first comparator is used first, and if two resources are equal then the
+      * second comparator is used.
+      *
+      * @param  <R>         the type of resources (typically {@code Feature}) to sort.
+      * @param  sort        the first "sort by" to use, or {@code null} if none.
+      * @param  comparator  the second "sort by" to use.
+      * @return concatenation of the two comparators.
+      */
+     @SuppressWarnings("unchecked")
+     public static <R> SortBy<? super R> concatenate(final SortBy<? super R> sort, final Comparator<? super R> comparator) {
+         final SortBy<? super R> other;
+         if (comparator instanceof SortBy<?>) {
+             other = (SortBy<? super R>) comparator;
+         } else if (comparator instanceof SortProperty<?>) {
+             other = new SortByComparator<>((SortProperty<? super R>) comparator);
+         } else {
+             return null;
+         }
+         if (sort == null) {
+             return other;
+         }
+         /*
+          * The (SortBy<R>) casts are unsafe — they should be (SortBy<? super R>) — but are okay in this context
+          * because we create a `SortByComparator` which will only "push" instances of R to the comparators.
+          * There is no code that "pull" instances of R from the comparators, consequently no code that may
+          * be surprised to get an instance of a super type of R instead of R.
+          */
+         return concatenate((SortBy<R>) sort, (SortBy<R>) other);
+     }
+ 
+     /**
+      * Creates a new comparator as the concatenation of the two given comparators.
+      * The first comparator is used first, and if two resources are equal then the
+      * second comparator is used.
+      *
+      * @param  <R>  the type of resources (typically {@code Feature}) to sort.
+      * @param  s1   the first "sort by" to use.
+      * @param  s2   the second "sort by" to use.
+      * @return concatenation of the two comparators.
+      */
+     public static <R> SortBy<R> concatenate(final SortBy<R> s1, final SortBy<R> s2) {
+         final Map<ValueReference<R,?>, SortProperty<R>> merged = new LinkedHashMap<>();
+         addAll(s1.getSortProperties(), merged);
+         addAll(s2.getSortProperties(), merged);
+         return new SortByComparator<>(merged);
+     }
+ 
+     /**
+      * Adds all elements of the {@code properties} list into the {@code merged} map.
+      * If two {@code SortProperty} instances use the same {@link ValueReference},
+      * then only the first occurrence is retained.
+      */
+     private static <R> void addAll(final List<SortProperty<R>> properties,
+             final Map<ValueReference<R,?>, SortProperty<R>> merged)
+     {
+         final int size = properties.size();
+         for (int i=0; i<size; i++) {
+             final SortProperty<R> p = properties.get(i);
+             ArgumentChecks.ensureNonNullElement("properties", i, p);
+             merged.putIfAbsent(p.getValueReference(), p);
+         }
+     }
+ 
+     /**
+      * Returns the properties whose values are used for sorting.
+      * The list shall have a minimum of one element.
+      */
+     @Override
+     public List<SortProperty<R>> getSortProperties() {
+         return UnmodifiableArrayList.wrap(properties);
+     }
+ 
+     /**
+      * Compares two resources for order. Returns a negative number if {@code r1} should be sorted before {@code r2},
+      * a positive number if {@code r2} should be after {@code r1}, or 0 if both resources are equal.
+      * The ordering of null resources or null property values is unspecified.
+      */
+     @Override
+     public int compare(final R r1, final R r2) {
+         for (final SortProperty<R> p : properties) {
+             final int c = p.compare(r1, r2);
+             if (c != 0) return c;
+         }
+         return 0;
+     }
+ 
+     /**
+      * Returns a comparator as the concatenation of this comparator with the given one.
+      * This comparator is used first, and if two resources are equal then the other comparator is used.
+      *
+      * @param  other  the other comparator to be used when this comparator considers two resources as equal.
+      * @return concatenation of this comparator with the given one.
+      */
+     @Override
+     @SuppressWarnings("unchecked")
+     public Comparator<R> thenComparing(final Comparator<? super R> other) {
+         if (other instanceof SortBy<?>) {
+             /*
+              * The (SortBy<R>) cast is unsafe — it should be (SortBy<? super R>) — but it is okay in this context
+              * because we create a `SortByComparator` which will only "push" instances of R to the `other` comparator.
+              * There is no code that "pull" instances of R from the `other` comparator, consequently no code that may
+              * be surprised to get an instance of a super type of R instead of R.
+              */
+             return concatenate(this, (SortBy<R>) other);
+         }
+         return SortBy.super.thenComparing(other);
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/Visitor.java
index 0000000000,b314efe3de..f6b47d6f17
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/Visitor.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/Visitor.java
@@@ -1,0 -1,329 +1,328 @@@
+ /*
+  * 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.filter.privy;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.function.BiConsumer;
+ import org.apache.sis.feature.internal.Resources;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.CodeList;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.LogicalOperatorName;
 -import org.opengis.filter.SpatialOperatorName;
 -import org.opengis.filter.DistanceOperatorName;
 -import org.opengis.filter.TemporalOperatorName;
 -import org.opengis.filter.ComparisonOperatorName;
++// Specific to the main branch:
++import org.apache.sis.filter.Filter;
++import org.apache.sis.filter.Expression;
++import org.apache.sis.pending.geoapi.filter.LogicalOperatorName;
++import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
++import org.apache.sis.pending.geoapi.filter.DistanceOperatorName;
++import org.apache.sis.pending.geoapi.filter.TemporalOperatorName;
++import org.apache.sis.pending.geoapi.filter.ComparisonOperatorName;
+ 
+ 
+ /**
+  * An executor of different actions depending on filter or expression type.
+  * Action are defined by {@link BiConsumer} where the first parameter is the filter or expression,
+  * and the second parameter is an arbitrary object used as accumulator. For example, the accumulator
+  * may be a {@link StringBuilder} where the filter is written as a SQL or CQL statement.
+  *
+  * <h2>Relationship with the visitor pattern</h2>
+  * This class provides similar functionalities than the "visitor pattern".
+  * The actions are defined by lambda functions in a {@link HashMap} instead of by overriding methods,
+  * but the results are similar.
+  *
+  * <h2>Thread-safety</h2>
+  * {@code Visitor} instances are thread-safe if protected methods are invoked at construction time only.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
++ * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
+  * @param  <A>  type of the accumulator object where actions will write their results.
+  */
+ public abstract class Visitor<R,A> {
+     /**
+      * All filters known to this visitor.
+      * May contain an entry associated to the {@code null} key.
+      *
 -     * @see #setFilterHandler(CodeList, BiConsumer)
++     * @see #setFilterHandler(Enum, BiConsumer)
+      */
 -    private final Map<CodeList<?>, BiConsumer<Filter<R>,A>> filters;
++    private final Map<Enum<?>, BiConsumer<Filter<R>,A>> filters;
+ 
+     /**
+      * All expressions known to this visitor.
+      * May contain an entry associated to the {@code null} key.
+      *
+      * @see #setExpressionHandler(String, BiConsumer)
+      */
+     private final Map<String, BiConsumer<Expression<R,?>,A>> expressions;
+ 
+     /**
+      * Creates a new visitor.
+      */
+     protected Visitor() {
+         filters     = new HashMap<>();
+         expressions = new HashMap<>();
+     }
+ 
+     /**
+      * Creates a new visitor which will accept only the specified type of objects.
+      *
+      * @param  hasFilters      whether this filter will accepts filters.
+      * @param  hasExpressions  whether this filter will accepts expressions.
+      */
+     protected Visitor(final boolean hasFilters, final boolean hasExpressions) {
+         filters     = hasFilters     ? new HashMap<>() : Collections.emptyMap();
+         expressions = hasExpressions ? new HashMap<>() : Collections.emptyMap();
+     }
+ 
+     /**
+      * Creates a new visitor initialized to the same handlers as the specified visitor.
+      * This constructor can be used for creating a copy from a template before to add or remove handlers.
+      * If any of the {@code copy} argument is {@code false}, it is caller's responsibility to not modify
+      * the corresponding map of handlers because it will be an instance shared with the {@code source}.
+      *
+      * @param  source           the visitor from which to copy the handlers.
+      * @param  copyFilters      whether to copy the map of filter handlers.
+      * @param  copyExpressions  whether to copy the map of expression handlers.
+      *
+      * @see #removeFilterHandlers(Collection)
+      */
+     protected Visitor(final Visitor<R,A> source, final boolean copyFilters, final boolean copyExpressions) {
+         filters     = copyFilters     ? new HashMap<>(source.filters)     : source.filters;
+         expressions = copyExpressions ? new HashMap<>(source.expressions) : source.expressions;
+     }
+ 
+     /**
+      * Returns the action to execute for the given type of filter.
+      * The {@code null} type is legal and identifies the action to execute
+      * when the {@link Filter} instance is null or has a null type.
+      *
+      * @param  type  identification of the filter type (can be {@code null}).
+      * @return the action to execute when the identified filter is found, or {@code null} if none.
+      */
 -    protected final BiConsumer<Filter<R>,A> getFilterHandler(final CodeList<?> type) {
++    protected final BiConsumer<Filter<R>,A> getFilterHandler(final Enum<?> type) {
+         return filters.get(type);
+     }
+ 
+     /**
+      * Returns the action to execute for the given type of expression.
+      * The {@code null} type is legal and identifies the action to execute
+      * when the {@link Expression} instance is null.
+      *
+      * @param  type  identification of the expression type (can be {@code null}).
+      * @return the action to execute when the identified expression is found, or {@code null} if none.
+      */
+     protected final BiConsumer<Expression<R,?>,A> getExpressionHandler(final String type) {
+         return expressions.get(type);
+     }
+ 
+     /**
+      * Sets the action to execute for the given type of filter.
+      * The {@code null} type is legal and identifies the action to execute
+      * when the {@link Filter} instance is null or has a null type.
+      *
+      * @param  type    identification of the filter type (can be {@code null}).
+      * @param  action  the action to execute when the identified filter is found.
+      */
 -    protected final void setFilterHandler(final CodeList<?> type, final BiConsumer<Filter<R>,A> action) {
++    protected final void setFilterHandler(final Enum<?> type, final BiConsumer<Filter<R>,A> action) {
+         filters.put(type, action);
+     }
+ 
+     /**
+      * Sets the same action for all member of the same family of filters.
+      * The action is set in enumeration declaration order up to the last type inclusive.
+      *
+      * @param  lastType  identification of the last filter type (inclusive).
+      * @param  action    the action to execute when an identified filter is found.
+      */
 -    private void setFamilyHandlers(final CodeList<?> lastType, final BiConsumer<Filter<R>,A> action) {
 -        for (final CodeList<?> type : lastType.family()) {
++    private void setFamilyHandlers(final Enum<?> lastType, final BiConsumer<Filter<R>,A> action) {
++        for (final Enum<?> type : lastType.getClass().getEnumConstants()) {
+             filters.put(type, action);
+             if (type == lastType) break;
+         }
+     }
+ 
+     /**
+      * Sets the action to execute for the given type of expression.
+      * The {@code null} type is legal and identifies the action to execute
+      * when the {@link Expression} instance is null.
+      *
+      * @param  type    identification of the expression type (can be {@code null}).
+      * @param  action  the action to execute when the identified expression is found.
+      */
+     protected final void setExpressionHandler(final String type, final BiConsumer<Expression<R,?>,A> action) {
+         expressions.put(type, action);
+     }
+ 
+     /**
+      * Sets the same action to execute for the given types of expression.
+      *
+      * @param  types   identification of the expression types.
+      * @param  action  the action to execute when the identified expression is found.
+      */
+     private void setExpressionHandlers(final BiConsumer<Expression<R,?>,A> action, final String... types) {
+         for (final String type : types) {
+             expressions.put(type, action);
+         }
+     }
+ 
+     /**
+      * Sets the same action to execute for the {@code AND},  {@code OR} and {@code NOT} types filter.
+      *
+      * @param  action  the action to execute when one of the enumerated filters is found.
+      */
+     protected final void setLogicalHandlers(final BiConsumer<Filter<R>,A> action) {
+         setFamilyHandlers(LogicalOperatorName.NOT, action);
+     }
+ 
+     /**
+      * Sets the same action for both the {@code IsNull} and {@code IsNil} types of filter.
+      *
+      * @param  action  the action to execute when one of the enumerated filters is found.
+      */
+     protected final void setNullAndNilHandlers(final BiConsumer<Filter<R>,A> action) {
 -        setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NULL), action);
 -        setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NIL),  action);
++        setFilterHandler(ComparisonOperatorName.PROPERTY_IS_NULL, action);
++        setFilterHandler(ComparisonOperatorName.PROPERTY_IS_NIL,  action);
+     }
+ 
+     /**
+      * Sets the same action to execute for the &lt;, &gt;, ≤, ≥, = and ≠ types of filter.
+      *
+      * @param  action  the action to execute when one of the enumerated filters is found.
+      */
+     protected final void setBinaryComparisonHandlers(final BiConsumer<Filter<R>,A> action) {
+         setFamilyHandlers(ComparisonOperatorName.PROPERTY_IS_GREATER_THAN_OR_EQUAL_TO, action);
+     }
+ 
+     /**
+      * Sets the same action to execute for the temporal comparison operators.
+      * The operators are {@code AFTER}, {@code BEFORE}, {@code BEGINS}, {@code BEGUN_BY}, {@code CONTAINS},
+      * {@code DURING}, {@code EQUALS}, {@code OVERLAPS}, {@code MEETS}, {@code ENDS}, {@code OVERLAPPED_BY},
+      * {@code MET_BY}, {@code ENDED_BY} and {@code ANY_INTERACTS} types filter.
+      *
+      * @param  action  the action to execute when one of the enumerated filters is found.
+      */
+     protected final void setBinaryTemporalHandlers(final BiConsumer<Filter<R>,A> action) {
+         setFamilyHandlers(TemporalOperatorName.ANY_INTERACTS, action);
+     }
+ 
+     /**
+      * Sets the same action to execute for the spatial comparison operators, including the ones
+      * with a distance parameter. The operators are {@code BBOX}, {@code EQUALS}, {@code DISJOINT},
+      * {@code INTERSECTS}, {@code TOUCHES}, {@code CROSSES}, {@code WITHIN}, {@code CONTAINS},
+      * {@code OVERLAPS}, {@code DWITHIN} and {@code BEYOND} types filter.
+      *
+      * @param  action  the action to execute when one of the enumerated filters is found.
+      */
+     protected final void setSpatialHandlers(final BiConsumer<Filter<R>,A> action) {
+         setFamilyHandlers(SpatialOperatorName.OVERLAPS, action);
+         setFamilyHandlers(DistanceOperatorName.WITHIN,  action);
+     }
+ 
+     /**
+      * Sets the same action to execute for the +, −, × and ÷ expressions.
+      *
+      * @param  action  the action to execute when one of the enumerated expressions is found.
+      */
+     protected final void setMathHandlers(final BiConsumer<Expression<R,?>,A> action) {
+         setExpressionHandlers(action, FunctionNames.Add, FunctionNames.Subtract, FunctionNames.Multiply, FunctionNames.Divide);
+     }
+ 
+     /**
+      * Removes all filters of the given types. Types that have no registered handlers are ignored.
+      *
+      * @param  types  types of filters to remove.
+      */
 -    protected final void removeFilterHandlers(final Collection<? extends CodeList<?>> types) {
++    protected final void removeFilterHandlers(final Collection<? extends Enum<?>> types) {
+         filters.keySet().removeAll(types);
+     }
+ 
+     /**
+      * Executes the registered action for the given filter.
+      * Actions are registered by calls to {@code setFooHandler(…)} before the call to this {@code visit(…)} method.
+      *
+      * <h4>Note on parameterized type</h4>
+      * This method sometimes needs to be invoked with instances of {@code Filter<? super R>},
+      * because this is the type of predicate expected by {@link java.util.function} and {@link java.util.stream}.
+      * But the parameterized type expected by this method matches the parameterized type of handlers registered by
 -     * {@link #setFilterHandler(CodeList, BiConsumer)} and similar methods, which use the exact {@code <R>} type.
++     * {@link #setFilterHandler(Enum, BiConsumer)} and similar methods, which use the exact {@code <R>} type.
+      * This restriction exists because when doing otherwise, parameterized types become hard to express in Java
+      * (we get a cascade of {@code super} keywords, something like {@code <? super ? super R>}).
+      * However, doing the {@code (Filter<R>) filter} cast is actually safe if the handlers do not invoke any
+      * {@code filter} method having a return value (directly or indirectly as list elements) restricted to
+      * the exact {@code <R>} type. Such methods do not exist in the GeoAPI interfaces, so the cast is safe
+      * if the {@link BiConsumer} handlers do not invoke implementation-specific methods.
+      * Since only subclasses known the details of registered handlers,
+      * the decision to cast or not is left to those subclasses.
+      *
+      * @param  filter       the filter for which to execute an action based on its type.
+      * @param  accumulator  where to write the result of all actions.
+      * @throws UnsupportedOperationException if there is no action registered for the given filter.
+      */
+     public void visit(final Filter<R> filter, final A accumulator) {
 -        final CodeList<?> type = (filter != null) ? filter.getOperatorType() : null;
++        final Enum<?> type = (filter != null) ? filter.getOperatorType() : null;
+         final BiConsumer<Filter<R>, A> f = filters.get(type);
+         if (f != null) {
+             f.accept(filter, accumulator);
+         } else {
+             typeNotFound(type, filter, accumulator);
+         }
+     }
+ 
+     /**
+      * Executes the registered action for the given expression.
+      * Actions are registered by calls to {@code setFooHandler(…)} before the call to this {@code visit(…)} method.
+      *
+      * @param  expression   the expression for which to execute an action based on its type.
+      * @param  accumulator  where to write the result of all actions.
+      * @throws UnsupportedOperationException if there is no action registered for the given expression.
+      */
+     public void visit(final Expression<R,?> expression, final A accumulator) {
+         final String type = (expression != null) ? expression.getFunctionName().tip().toString() : null;
+         final BiConsumer<Expression<R,?>, A> f = expressions.get(type);
+         if (f != null) {
+             f.accept(expression, accumulator);
+         } else {
+             typeNotFound(type, expression, accumulator);
+         }
+     }
+ 
+     /**
+      * Adds the value to use or throws an exception when there is no action registered for a given filter type.
+      * The default implementation throws {@link UnsupportedOperationException}.
+      *
+      * @param  type         the filter type which has not been found, or {@code null} if {@code filter} is null.
+      * @param  filter       the filter (may be {@code null}).
+      * @param  accumulator  where to write the result of all actions.
+      * @throws UnsupportedOperationException if there is no default action.
+      */
 -    protected void typeNotFound(final CodeList<?> type, final Filter<R> filter, final A accumulator) {
++    protected void typeNotFound(final Enum<?> type, final Filter<R> filter, final A accumulator) {
+         throw new UnsupportedOperationException(Resources.format(Resources.Keys.CanNotVisit_2, 0, type));
+     }
+ 
+     /**
+      * Adds the value to use or throws an exception when there is no action registered for a given expression type.
+      * The default implementation throws {@link UnsupportedOperationException}.
+      *
+      * @param  type         the expression type which has not been found, or {@code null} if {@code expression} is null.
+      * @param  expression   the expression (may be {@code null}).
+      * @param  accumulator  where to write the result of all actions.
+      * @throws UnsupportedOperationException if there is no default value.
+      */
+     protected void typeNotFound(final String type, final Expression<R,?> expression, final A accumulator) {
+         throw new UnsupportedOperationException(Resources.format(Resources.Keys.CanNotVisit_2, 1, type));
+     }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
index e72df82622,3e56cef2da..40728be85a
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
@@@ -27,13 -27,14 +27,13 @@@ import org.apache.sis.referencing.facto
  import org.apache.sis.feature.builder.FeatureTypeBuilder;
  import org.apache.sis.feature.builder.PropertyTypeBuilder;
  import org.apache.sis.feature.builder.AttributeTypeBuilder;
- import org.apache.sis.util.internal.Constants;
+ import org.apache.sis.util.privy.Constants;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.FeatureType;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.InvalidFilterValueException;
 +// Specific to the main branch:
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.pending.geoapi.filter.Literal;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
index 11d52cb6de,632b473cdf..c2c749e39a
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
@@@ -19,11 -19,12 +19,11 @@@ package org.apache.sis.filter.sqlmm
  import java.util.Arrays;
  import java.util.Collection;
  import org.apache.sis.geometry.wrapper.Geometries;
- import org.apache.sis.filter.internal.FunctionRegister;
+ import org.apache.sis.filter.FunctionRegister;
  import org.apache.sis.pending.jdk.JDK16;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.capability.AvailableFunction;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Point.java
index 41f720bfbd,9aec4e3105..efe88a1f58
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Point.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Point.java
@@@ -22,11 -22,12 +22,11 @@@ import static java.lang.Double.isNaN
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.apache.sis.geometry.wrapper.Geometries;
  import org.apache.sis.geometry.wrapper.GeometryWrapper;
- import org.apache.sis.util.internal.UnmodifiableArrayList;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.InvalidFilterValueException;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
index 639e994330,4076c8b663..e74b1642b4
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/SpatialOperationContext.java
@@@ -44,12 -44,12 +44,12 @@@ import org.apache.sis.referencing.opera
  import org.apache.sis.referencing.crs.DefaultProjectedCRS;
  import org.apache.sis.measure.Units;
  import org.apache.sis.util.resources.Errors;
- import org.apache.sis.util.internal.Constants;
+ import org.apache.sis.util.privy.Constants;
  import org.apache.sis.metadata.iso.citation.Citations;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.SpatialOperatorName;
 -import org.opengis.filter.DistanceOperatorName;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
 +import org.apache.sis.pending.geoapi.filter.DistanceOperatorName;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index b11150a1ba,df20efc692..96f54f796d
--- 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
@@@ -45,9 -45,9 +45,9 @@@ import org.opengis.util.FactoryExceptio
  import org.opengis.geometry.DirectPosition;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.opengis.referencing.operation.CoordinateOperation;
 -import org.opengis.referencing.operation.TransformException;
  import org.opengis.referencing.operation.MathTransform;
 +import org.opengis.referencing.operation.TransformException;
- import org.apache.sis.referencing.util.ReferencingUtilities;
+ import org.apache.sis.referencing.privy.ReferencingUtilities;
  import org.apache.sis.geometry.DirectPosition2D;
  import org.apache.sis.geometry.GeneralDirectPosition;
  import org.apache.sis.geometry.GeneralEnvelope;
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
index 278575cdb3,4c04121af6..f9c8ba8df5
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
@@@ -44,11 -44,14 +44,11 @@@ import org.apache.sis.util.ArraysExt
  import org.apache.sis.util.resources.Messages;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.measure.NumberRange;
- import org.apache.sis.util.internal.Numerics;
+ import org.apache.sis.util.privy.Numerics;
  import org.apache.sis.feature.internal.Resources;
- import org.apache.sis.coverage.grid.j2d.ImageUtilities;
+ import org.apache.sis.coverage.privy.ImageUtilities;
  import static org.apache.sis.pending.jdk.JDK18.ceilDiv;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.grid.SequenceType;
 -
  
  /**
   * An iterator over sample values in a raster or an image.  This iterator makes easier to read and write efficiently
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/pending/geoapi/filter/Name.java
index 2c0201bc90,0000000000..a2d6c7cf29
mode 100644,000000..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/pending/geoapi/filter/Name.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/pending/geoapi/filter/Name.java
@@@ -1,41 -1,0 +1,41 @@@
 +/*
 + * 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.pending.geoapi.filter;
 +
 +import org.opengis.util.LocalName;
 +import org.opengis.util.ScopedName;
- import org.apache.sis.filter.internal.FunctionNames;
++import org.apache.sis.filter.privy.FunctionNames;
 +import org.apache.sis.util.iso.Names;
 +
 +
 +/**
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
 + */
 +@SuppressWarnings("doclint:missing")
 +public final class Name {
 +    static final LocalName STANDARD = Names.createLocalName(null, null, "fes");
 +
 +    static final LocalName EXTENSION = Names.createLocalName(null, null, "extension");
 +
 +    public static final ScopedName LITERAL = Names.createScopedName(STANDARD, null, FunctionNames.Literal);
 +
 +    public static final ScopedName VALUE_REFERENCE = Names.createScopedName(STANDARD, null, FunctionNames.ValueReference);
 +
 +    private Name() {
 +    }
 +}
diff --cc endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/privy/FunctionNamesTest.java
index 0000000000,f3e8064066..27072d925f
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/privy/FunctionNamesTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/privy/FunctionNamesTest.java
@@@ -1,0 -1,154 +1,67 @@@
+ /*
+  * 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.filter.privy;
+ 
+ import java.lang.reflect.Field;
+ import org.apache.sis.filter.sqlmm.SQLMM;
+ 
+ // Test dependencies
+ import org.junit.jupiter.api.Test;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import java.util.List;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.ComparisonOperator;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.BetweenComparisonOperator;
 -import org.opengis.filter.NullOperator;
 -import org.opengis.filter.NilOperator;
 -import org.opengis.filter.LikeOperator;
 -import org.opengis.filter.Expression;
 -
+ 
+ /**
+  * Verifies the values declared in {@link FunctionNames}.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class FunctionNamesTest extends TestCase {
+     /**
+      * Creates a new test case.
+      */
+     public FunctionNamesTest() {
+     }
+ 
+     /**
+      * Verifies that each field has the same name as its value.
+      *
+      * @throws IllegalAccessException should never happen.
+      */
+     @Test
+     public void verifyFieldNames() throws IllegalAccessException {
+         for (final Field f : FunctionNames.class.getFields()) {
+             assertEquals(f.getName(), f.get(null));
+         }
+     }
+ 
 -    /**
 -     * Base class for dummy implementation of filter.
 -     */
 -    private static abstract class FilterBase implements ComparisonOperator<Object> {
 -        @Override public List<Expression<Object,?>> getExpressions() {return List.of();}
 -        @Override public Class<Object> getResourceClass() {return Object.class;}
 -        @Override public boolean test(Object resource) {return false;}
 -    }
 -
 -    /**
 -     * Verifies the {@value FunctionNames#PROPERTY_IS_NULL} name.
 -     */
 -    @Test
 -    public void verifyPropertyIsNull() {
 -        final class Instanciable extends FilterBase implements NullOperator<Object> {}
 -        assertSame(new Instanciable().getOperatorType(), ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NULL));
 -    }
 -
 -    /**
 -     * Verifies the {@value FunctionNames#PROPERTY_IS_NIL} name.
 -     */
 -    @Test
 -    public void verifyPropertyIsNil() {
 -        final class Instanciable extends FilterBase implements NilOperator<Object> {}
 -        assertSame(new Instanciable().getOperatorType(), ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NIL));
 -    }
 -
 -    /**
 -     * Verifies the {@value FunctionNames#PROPERTY_IS_LIKE} name.
 -     */
 -    @Test
 -    public void verifyPropertyIsLike() {
 -        final class Instanciable extends FilterBase implements LikeOperator<Object> {}
 -        assertSame(new Instanciable().getOperatorType(), ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_LIKE));
 -    }
 -
 -    /**
 -     * Verifies the {@value FunctionNames#PROPERTY_IS_BETWEEN} name.
 -     */
 -    @Test
 -    public void verifyPropertyIsBetween() {
 -        final class Instanciable extends FilterBase implements BetweenComparisonOperator<Object> {}
 -        assertSame(new Instanciable().getOperatorType(), ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN));
 -    }
 -
 -    /**
 -     * Verifies the {@value FunctionNames#Literal} name.
 -     */
 -    @Test
 -    public void verifyLiteral() {
 -        final var expression = new Literal<Object,Object>() {
 -            @Override public Object getValue() {return null;}
 -            @Override public <N> Expression<Object, N> toValueType(Class<N> target) {
 -                throw new UnsupportedOperationException();
 -            }
 -        };
 -        assertEquals(expression.getFunctionName().tip().toString(), FunctionNames.Literal);
 -    }
 -
 -    /**
 -     * Verifies the {@value FunctionNames#ValueReference} name.
 -     */
 -    @Test
 -    public void verifyValueReference() {
 -        final var expression = new ValueReference<Object,Object>() {
 -            @Override public String getXPath()      {return null;}
 -            @Override public Object apply(Object o) {return null;}
 -            @Override public Class<Object> getResourceClass() {return Object.class;}
 -            @Override public <N> Expression<Object,N> toValueType(Class<N> target) {
 -                throw new UnsupportedOperationException();
 -            }
 -        };
 -        assertEquals(expression.getFunctionName().tip().toString(), FunctionNames.ValueReference);
 -    }
 -
+     /**
+      * Verifies SQLMM names.
+      */
+     @Test
+     public void verifySQLMM() {
+         int count = 0;
+         for (final Field f : FunctionNames.class.getFields()) {
+             final String name = f.getName();
+             if (name.startsWith("ST_")) {
+                 assertEquals(name, SQLMM.valueOf(name).name());
+                 count++;
+             }
+         }
+         assertEquals(8, count);
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/main/module-info.java
index 601c0441d6,0105a2f2f9..a87c6f380c
--- a/endorsed/src/org.apache.sis.metadata/main/module-info.java
+++ b/endorsed/src/org.apache.sis.metadata/main/module-info.java
@@@ -45,14 -45,14 +45,14 @@@ module org.apache.sis.metadata 
      provides org.apache.sis.xml.bind.TypeRegistration
          with org.apache.sis.metadata.internal.MetadataTypes;
  
-     provides org.apache.sis.util.internal.MetadataServices
+     provides org.apache.sis.util.privy.MetadataServices
          with org.apache.sis.metadata.internal.ServicesForUtility;
  
-     uses org.apache.sis.metadata.internal.ReferencingServices;
-     uses org.apache.sis.metadata.sql.util.Initializer;
+     uses org.apache.sis.metadata.privy.ReferencingServices;
+     uses org.apache.sis.metadata.sql.privy.Initializer;
      uses org.apache.sis.xml.bind.AdapterReplacement;
      uses org.apache.sis.xml.bind.TypeRegistration;
 -    uses org.opengis.temporal.TemporalFactory;
 +    uses org.apache.sis.pending.geoapi.temporal.TemporalFactory;
  
      /*
       * Public API open to everyone.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java
index 431cfa789b,11c2ec435a..5b791fcaf5
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java
@@@ -19,11 -19,11 +19,11 @@@ package org.apache.sis.metadata
  import java.util.Iterator;
  import java.util.Collection;
  import org.apache.sis.util.Emptiable;
- import org.apache.sis.util.internal.CollectionsExt;
+ import org.apache.sis.util.privy.CollectionsExt;
  import static org.apache.sis.metadata.ValueExistencePolicy.*;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.ControlledVocabulary;
 +// Specific to the main branch:
 +import org.opengis.util.CodeList;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/CitationConstant.java
index 0000000000,4be6930510..7e8e848a2a
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/CitationConstant.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/CitationConstant.java
@@@ -1,0 -1,244 +1,238 @@@
+ /*
+  * 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.metadata.internal;
+ 
+ import java.util.Date;
+ import java.util.Collection;
+ import java.util.logging.Logger;
+ import java.io.ObjectStreamException;
+ import org.opengis.metadata.Identifier;
+ import org.opengis.metadata.citation.Citation;
+ import org.opengis.metadata.citation.CitationDate;
+ import org.opengis.metadata.citation.PresentationForm;
+ import org.opengis.metadata.citation.Series;
+ import org.opengis.util.InternationalString;
+ import org.apache.sis.xml.IdentifierSpace;
+ import org.apache.sis.metadata.sql.MetadataSource;
+ import org.apache.sis.metadata.sql.MetadataStoreException;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.metadata.simple.SimpleCitation;
+ import org.apache.sis.system.Loggers;
+ import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.util.logging.Logging;
+ 
+ // Specific to the main and geoapi-3.1 branches:
+ import org.opengis.metadata.citation.ResponsibleParty;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.citation.OnlineResource;
 -import org.opengis.metadata.identification.BrowseGraphic;
 -
+ 
+ /**
+  * Base class for the {@code public static final Citation} constants defined in some SIS classes.
+  * This base class contains only an abbreviation (e.g. {@code "OGC"} or {@code "EPSG"}) which can
+  * be used as the primary key where to search for more information in a database. If no database
+  * is available, then that simple primary key will be used as the citation title.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  *
+  * @see IdentifierSpace
+  * @see Citations
+  */
+ public class CitationConstant extends SimpleCitation {
+     /**
+      * For cross-version compatibility.
+      */
+     private static final long serialVersionUID = -8429121584437634107L;
+ 
+     /**
+      * Where to log warnings when searching for an entry in the database.
+      */
+     private static final Logger LOGGER = Logger.getLogger(Loggers.SQL);
+ 
+     /**
+      * Class of {@code public static final Citation} constants which are also used as namespace for identifiers.
+      * The most typical example is the "EPSG" authority which manage the codes identifying Coordinate Reference
+      * System (CRS) objects in the EPSG namespace.
+      *
+      * @param <T>  the type of object used as identifier values.
+      */
+     public static class Authority<T> extends CitationConstant implements IdentifierSpace<T> {
+         /**
+          * For cross-version compatibility.
+          */
+         private static final long serialVersionUID = 2067932813561130294L;
+ 
+         /**
+          * Creates a new citation for an authority managing codes in the given namespace.
+          * This constructor assumes that the namespace is the same as the abbreviation given as citation title.
+          *
+          * @param  namespace  the namespace of codes managed by this authority (e.g. "EPSG").
+          */
+         public Authority(final String namespace) {
+             super(namespace);
+         }
+ 
+         /**
+          * Creates a new citation for an authority managing codes in the given namespace.
+          *
+          * @param  name       a human-understandable primary key for fetching more information.
+          * @param  namespace  the namespace of codes managed by this authority (e.g. "EPSG").
+          */
+         public Authority(final String name, final String namespace) {
+             super(name, namespace);
+         }
+ 
+         /**
+          * Returns the name space given at construction time. Can be one of the following:
+          * <ul>
+          *   <li>Abbreviation of the authority managing the codes (e.g. {@code "EPSG"} or {@code "ISBN"}).</li>
+          *   <li>XML attribute name with its prefix (e.g. {@code "gml:id"}, {@code "gco:uuid"} or {@code "xlink:href"}).</li>
+          * </ul>
+          */
+         @Override
+         public final String getName() {
+             return namespace;
+         }
+ 
+         /**
+          * Returns a string representation of this identifier space.
+          */
+         @Override
+         public final String toString() {
+             return Strings.bracket(IdentifierSpace.class, title);
+         }
+     }
+ 
+     /**
+      * The name by which this citation is known to {@link Citations#fromName(String)}.
+      * Often the same as the abbreviation that {@link CitationConstant} uses as the title.
+      * If this {@code CitationConstant} is a {@link Authority} subtype, then this is also
+      * the authority namespace.
+      */
+     public final String namespace;
+ 
+     /**
+      * The citation which contain the "real" data, or {@code null} if not yet created.
+      * This is usually an instance created by {@link MetadataSource}. Those instances
+      * manage their own caching, so accesses to the database should not occur often.
+      */
+     private transient volatile Citation delegate;
+ 
+     /**
+      * Creates a new proxy for the given primary key.
+      * The key should be readable enough for being usable as a fallback if the database is not available.
+      *
+      * @param  name  a human-understandable primary key for fetching more information.
+      */
+     public CitationConstant(final String name) {
+         super(name);
+         this.namespace = name;
+     }
+ 
+     /**
+      * Creates a new proxy for the given primary key but a different programmatic name.
+      * The key should be readable enough for being usable as a fallback if the database is not available.
+      *
+      * @param  name       a human-understandable primary key for fetching more information.
+      * @param  namespace  the name by which this citation is known to {@link Citations#fromName(String)}.
+      */
+     public CitationConstant(final String name, final String namespace) {
+         super(name);
+         this.namespace = namespace;
+     }
+ 
+     /**
+      * Notify this instance that the database content may have changed, or that the module path has changed.
+      */
+     public final void refresh() {
+         delegate = null;
+     }
+ 
+     /**
+      * Returns the citation instance which contain the actual data. That instance is provided by the
+      * {@code org.apache.sis.metadata} module, which is optional. If that module is not on the module path,
+      * then this {@code delegate()} method will use the few information provided by the current instance.
+      *
+      * <p>Note that it should be very rare to not have {@code org.apache.sis.metadata} on the module path,
+      * since that module is required by {@code org.apache.sis.referencing} which is itself required by
+      * almost all other SIS modules.</p>
+      */
+     @SuppressWarnings("DoubleCheckedLocking")
+     private Citation delegate() {
+         Citation c = delegate;
+         if (c == null) {
+             synchronized (this) {
+                 c = delegate;
+                 if (c == null) {
+                     try {
+                         c = MetadataSource.getProvided().lookup(Citation.class, title);
+                     } catch (MetadataStoreException e) {
+                         /*
+                          * If no database was available, MetadataSource.getProvided() was supposed to fallback on
+                          * the MetadataFallback class. So if we get this exception, a more serious error occurred.
+                          * It is still not fatal however, since most of Citation content is informative.
+                          */
+                         Logging.unexpectedException(LOGGER, CitationConstant.class, "delegate", e);
+                         c = new SimpleCitation(title);
+                     }
+                     delegate = c;
+                 }
+             }
+         }
+         return c;
+     }
+ 
+     /**
+      * Returns the title, which is mandatory.
+      */
+     @Override
+     public InternationalString getTitle() {
+         InternationalString title = delegate().getTitle();
+         return (title != null) ? title : super.getTitle();
+     }
+ 
+     /**
+      * Redirects the call to the delegate citation (the instance which actually contain the data).
+      *
+      * @return the value returned by the delegate.
+      */
+     @Override public Collection<? extends InternationalString>  getAlternateTitles()         {return delegate().getAlternateTitles();}
+     @Override public Collection<? extends CitationDate>         getDates()                   {return delegate().getDates();}
+     @Override public InternationalString                        getEdition()                 {return delegate().getEdition();}
+     @Override public Date                                       getEditionDate()             {return delegate().getEditionDate();}
+     @Override public Collection<? extends Identifier>           getIdentifiers()             {return delegate().getIdentifiers();}
+     @Override public Collection<? extends ResponsibleParty>     getCitedResponsibleParties() {return delegate().getCitedResponsibleParties();}
+     @Override public Collection<PresentationForm>               getPresentationForms()       {return delegate().getPresentationForms();}
+     @Override public Series                                     getSeries()                  {return delegate().getSeries();}
+     @Override public InternationalString                        getOtherCitationDetails()    {return delegate().getOtherCitationDetails();}
 -    @Override public Collection<? extends OnlineResource>       getOnlineResources()         {return delegate().getOnlineResources();}
 -    @Override public Collection<? extends BrowseGraphic>        getGraphics()                {return delegate().getGraphics();}
+     @Override public String                                     getISBN()                    {return delegate().getISBN();}
+     @Override public String                                     getISSN()                    {return delegate().getISSN();}
+ 
+     /**
+      * Invoked at deserialization time in order to replace the deserialized instance by the existing
+      * instance defined in the {@link org.apache.sis.metadata.iso.citation.Citations} class.
+      *
+      * @return the instance to use, as an unique instance if possible.
+      * @throws ObjectStreamException never thrown.
+      */
+     protected Object readResolve() throws ObjectStreamException {
+         final Citation c = Citations.fromName(title);
+         return (c instanceof CitationConstant) ? c : this;
+         /*
+          * Returns 'this' should happen only if the Citation has been serialized
+          * on a more recent version of Apache SIS than the current version.
+          */
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/ServicesForUtility.java
index 1da9e5395d,5b67ae91c1..e0497096ce
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/ServicesForUtility.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/ServicesForUtility.java
@@@ -19,22 -19,24 +19,24 @@@ package org.apache.sis.metadata.interna
  import java.text.Format;
  import java.util.Locale;
  import java.util.TimeZone;
 -import java.util.function.Supplier;
  import javax.sql.DataSource;
  import java.sql.SQLException;
 +import java.util.function.Supplier;
  import org.opengis.metadata.citation.Citation;
- import org.apache.sis.util.Exceptions;
  import org.apache.sis.util.Classes;
- import org.apache.sis.util.internal.MetadataServices;
- import org.apache.sis.metadata.sql.util.Initializer;
- import org.apache.sis.system.SystemListener;
- import org.apache.sis.xml.bind.Context;
- import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.util.Exceptions;
  import org.apache.sis.util.iso.Types;
+ import org.apache.sis.util.logging.Logging;
  import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.privy.MetadataServices;
+ import org.apache.sis.system.SystemListener;
+ import org.apache.sis.metadata.sql.privy.Initializer;
+ import org.apache.sis.metadata.privy.Identifiers;
+ import org.apache.sis.metadata.privy.ReferencingServices;
+ import org.apache.sis.xml.bind.Context;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.ControlledVocabulary;
 +// Specific to the main branch:
 +import org.opengis.util.CodeList;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultContact.java
index 4c66e3883f,99f6a6be5c..f63fedcbf8
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultContact.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultContact.java
@@@ -35,15 -35,11 +35,15 @@@ import org.apache.sis.xml.bind.FilterBy
  import org.apache.sis.xml.bind.gco.InternationalStringAdapter;
  import org.apache.sis.metadata.internal.Dependencies;
  import org.apache.sis.metadata.iso.legacy.LegacyPropertyAdapter;
- import org.apache.sis.xml.util.LegacyNamespaces;
- import org.apache.sis.util.internal.CollectionsExt;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
+ import org.apache.sis.util.privy.CollectionsExt;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.citation.TelephoneType;
 +// Specific to the main branch:
 +import org.opengis.util.CodeList;
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +import org.apache.sis.pending.geoapi.evolution.UnsupportedCodeList;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java
index 347896f084,491773332f..45cece31df
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java
@@@ -28,11 -28,17 +28,11 @@@ import org.opengis.metadata.citation.Re
  import org.opengis.metadata.citation.Role;
  import org.opengis.util.InternationalString;
  import org.apache.sis.util.iso.Types;
- import org.apache.sis.xml.util.LegacyNamespaces;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
  import org.apache.sis.metadata.internal.Dependencies;
  import org.apache.sis.metadata.iso.legacy.LegacyPropertyAdapter;
- import static org.apache.sis.metadata.internal.ImplementationHelper.valueIfDefined;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.valueIfDefined;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.citation.Party;
 -import org.opengis.metadata.citation.Individual;
 -import org.opengis.metadata.citation.Organisation;
 -import org.opengis.metadata.citation.Responsibility;
 -
  
  /**
   * Identification of, and means of communication with, person(s) and
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java
index 8a631e7162,34f33f2f92..220b7587ff
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java
@@@ -36,12 -36,10 +36,12 @@@ import org.apache.sis.metadata.internal
  import org.apache.sis.metadata.iso.legacy.LegacyPropertyAdapter;
  import org.apache.sis.xml.bind.FilterByVersion;
  import org.apache.sis.xml.bind.metadata.MD_Identifier;
- import static org.apache.sis.metadata.internal.ImplementationHelper.valueIfDefined;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.valueIfDefined;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.content.AttributeGroup;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
index 0c18db8760,5853bc5402..f6f78e7fde
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
@@@ -32,13 -32,10 +32,13 @@@ import org.apache.sis.xml.bind.lan.PT_L
  import org.apache.sis.xml.bind.lan.LocaleAndCharset;
  import org.apache.sis.metadata.internal.Dependencies;
  import org.apache.sis.metadata.iso.legacy.LegacyPropertyAdapter;
- import static org.apache.sis.metadata.internal.ImplementationHelper.valueIfDefined;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.valueIfDefined;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.content.FeatureTypeInfo;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Obligation.CONDITIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultFeatureTypeInfo.java
index 9ed23460fb,93c4f3e576..74ebd7fe46
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultFeatureTypeInfo.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultFeatureTypeInfo.java
@@@ -23,13 -23,10 +23,13 @@@ import org.opengis.util.GenericName
  import org.apache.sis.measure.ValueRange;
  import org.apache.sis.metadata.TitleProperty;
  import org.apache.sis.metadata.iso.ISOMetadata;
- import static org.apache.sis.metadata.internal.ImplementationHelper.ensurePositive;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.ensurePositive;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.content.FeatureTypeInfo;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Obligation.MANDATORY;
 +import static org.opengis.annotation.Specification.ISO_19115;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultSampleDimension.java
index 86015b6854,2be6ff4e80..fe1e286ac3
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultSampleDimension.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/content/DefaultSampleDimension.java
@@@ -35,13 -35,10 +35,13 @@@ import org.apache.sis.xml.bind.gco.GO_I
  import org.apache.sis.xml.bind.gco.GO_Record;
  import org.apache.sis.xml.bind.gco.GO_RecordType;
  import org.apache.sis.xml.bind.metadata.MI_RangeElementDescription;
- import static org.apache.sis.metadata.internal.ImplementationHelper.ensurePositive;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.ensurePositive;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.content.SampleDimension;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Obligation.CONDITIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultDigitalTransferOptions.java
index dcb550d127,de564eff23..77c6cb5978
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultDigitalTransferOptions.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultDigitalTransferOptions.java
@@@ -33,14 -33,9 +33,14 @@@ import org.apache.sis.measure.ValueRang
  import org.apache.sis.metadata.internal.Dependencies;
  import org.apache.sis.xml.bind.FilterByVersion;
  import org.apache.sis.xml.bind.gts.TM_PeriodDuration;
- import org.apache.sis.util.internal.CollectionsExt;
- import static org.apache.sis.metadata.internal.ImplementationHelper.ensurePositive;
+ import org.apache.sis.util.privy.CollectionsExt;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.ensurePositive;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +
  
  /**
   * Technical means and media by which a resource is obtained from the distributor.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultFormat.java
index 3fd87d27ac,e522d47dde..67063f1876
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultFormat.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultFormat.java
@@@ -31,18 -31,12 +31,18 @@@ import org.apache.sis.metadata.internal
  import org.apache.sis.metadata.iso.ISOMetadata;
  import org.apache.sis.metadata.iso.legacy.LegacyPropertyAdapter;
  import org.apache.sis.xml.bind.FilterByVersion;
- import org.apache.sis.xml.util.LegacyNamespaces;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
  import org.apache.sis.xml.bind.metadata.MD_Medium;
  import org.apache.sis.xml.bind.metadata.CI_Citation;
- import org.apache.sis.util.internal.CollectionsExt;
+ import org.apache.sis.util.privy.CollectionsExt;
  import org.apache.sis.metadata.iso.citation.DefaultCitation;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Obligation.MANDATORY;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +
  
  /**
   * Description of the computer language construct that specifies the representation
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultMedium.java
index 917c08b499,e3c2ed9667..fc4aed2af6
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultMedium.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultMedium.java
@@@ -41,14 -41,12 +41,14 @@@ import static org.apache.sis.metadata.p
  
  // Specific to the main and geoapi-3.1 branches:
  import org.opengis.metadata.distribution.MediumName;
- import org.apache.sis.util.internal.CodeLists;
+ import org.apache.sis.util.privy.CodeLists;
  import org.apache.sis.metadata.iso.citation.DefaultCitation;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import java.util.AbstractSet;
 -import java.util.Iterator;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
- import static org.apache.sis.metadata.internal.ImplementationHelper.valueIfDefined;
++import static org.apache.sis.metadata.privy.ImplementationHelper.valueIfDefined;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultStandardOrderProcess.java
index f1e29c28c6,9f2a0e9a88..a4e68742e0
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultStandardOrderProcess.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/distribution/DefaultStandardOrderProcess.java
@@@ -29,14 -29,9 +29,14 @@@ import org.opengis.metadata.distributio
  import org.apache.sis.xml.bind.gco.GO_RecordType;
  import org.apache.sis.xml.bind.gco.GO_Record;
  import org.apache.sis.metadata.iso.ISOMetadata;
- import static org.apache.sis.metadata.internal.ImplementationHelper.toDate;
- import static org.apache.sis.metadata.internal.ImplementationHelper.toMilliseconds;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.toDate;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.toMilliseconds;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +
  
  /**
   * Common ways in which the resource may be obtained or received, and related instructions
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java
index 83a0645c99,6d540c999d..9fe2c6a882
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java
@@@ -27,14 -27,9 +27,14 @@@ import org.opengis.metadata.extent.Geog
  import org.opengis.metadata.extent.GeographicBoundingBox;
  import org.opengis.metadata.extent.SpatialTemporalExtent;
  import org.opengis.referencing.operation.TransformException;
- import org.apache.sis.metadata.internal.ReferencingServices;
- import static org.apache.sis.metadata.internal.ImplementationHelper.valueIfDefined;
+ import org.apache.sis.metadata.privy.ReferencingServices;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.valueIfDefined;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +
  
  /**
   * Extent with respect to date/time and spatial boundaries.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
index 4ee83115e2,a3cb0d5f00..d649d03250
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
@@@ -66,12 -66,11 +66,12 @@@ import org.apache.sis.util.iso.Types
  import org.apache.sis.util.resources.Vocabulary;
  import org.apache.sis.util.resources.Errors;
  import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
- import static org.apache.sis.util.internal.CollectionsExt.nonNull;
- import static org.apache.sis.metadata.internal.ReferencingServices.AUTHALIC_RADIUS;
+ import static org.apache.sis.util.privy.CollectionsExt.nonNull;
+ import static org.apache.sis.metadata.privy.ReferencingServices.AUTHALIC_RADIUS;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.geometry.MismatchedReferenceSystemException;
 +// Specific to the main branch:
 +import org.opengis.metadata.identification.DataIdentification;
 +import org.apache.sis.pending.geoapi.evolution.Interim;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/DefaultCoupledResource.java
index e3bc720f6e,a87b1a60a5..214eac6692
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/DefaultCoupledResource.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/DefaultCoupledResource.java
@@@ -29,15 -29,14 +29,15 @@@ import org.apache.sis.metadata.iso.ISOM
  import org.apache.sis.xml.Namespaces;
  import org.apache.sis.xml.bind.FilterByVersion;
  import org.apache.sis.xml.bind.metadata.SV_OperationMetadata;
- import org.apache.sis.xml.util.LegacyNamespaces;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
  import org.apache.sis.xml.bind.gco.GO_GenericName;
- import org.apache.sis.util.internal.Constants;
+ import org.apache.sis.util.privy.Constants;
  import org.apache.sis.util.iso.Names;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.identification.CoupledResource;
 -import org.opengis.metadata.identification.OperationMetadata;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/DefaultResolution.java
index f31dfe6bd1,30eeff3d1b..5d040dd101
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/DefaultResolution.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/DefaultResolution.java
@@@ -30,13 -30,8 +30,13 @@@ import org.apache.sis.xml.bind.gco.Inte
  import org.apache.sis.metadata.iso.ISOMetadata;
  import org.apache.sis.measure.ValueRange;
  import org.apache.sis.util.resources.Messages;
- import static org.apache.sis.metadata.internal.ImplementationHelper.ensurePositive;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.ensurePositive;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.CONDITIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +
  
  /**
   * Level of detail expressed as a scale factor or a ground distance.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/OperationName.java
index ba04343c30,110dcdf744..31e09beb07
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/OperationName.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/identification/OperationName.java
@@@ -19,11 -19,19 +19,11 @@@ package org.apache.sis.metadata.iso.ide
  import java.util.Map;
  import java.util.HashMap;
  import java.util.Collection;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import java.util.Collections;
 -import java.io.Serializable;
 -import org.opengis.metadata.citation.OnlineResource;
 -import org.opengis.metadata.identification.CoupledResource;
 -import org.opengis.metadata.identification.DistributedComputingPlatform;
 -import org.opengis.metadata.identification.OperationMetadata;
 -
  
  /**
 - * An {@code OperationMetadata} placeholder to be replaced later by a reference to another {@link OperationMetadata}.
 + * An {@code OperationMetadata} placeholder to be replaced later by a reference to another {@code OperationMetadata}.
   * This temporary place holder is used when the operation name is unmarshalled before the actual operation definition.
   *
   * @author  Martin Desruisseaux (Geomatys)
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultScope.java
index 57cde0b37a,a58349d970..f576f3097c
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultScope.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/maintenance/DefaultScope.java
@@@ -26,14 -26,8 +26,14 @@@ import org.opengis.metadata.maintenance
  import org.apache.sis.metadata.iso.ISOMetadata;
  import org.apache.sis.xml.Namespaces;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.maintenance.Scope;
 +// Specific to the main branch:
 +import org.opengis.metadata.quality.Scope;
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +import org.apache.sis.metadata.internal.Dependencies;
 +import org.apache.sis.metadata.iso.legacy.LegacyPropertyAdapter;
- import org.apache.sis.util.internal.CollectionsExt;
++import org.apache.sis.util.privy.CollectionsExt;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/AbstractElement.java
index 648d8c2132,1042670b0a..8db20c843a
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/AbstractElement.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/AbstractElement.java
@@@ -43,14 -43,14 +43,14 @@@ import org.apache.sis.xml.bind.gco.Inte
  import org.apache.sis.metadata.iso.legacy.DateToTemporal;
  import org.apache.sis.metadata.iso.legacy.TemporalToDate;
  import org.apache.sis.metadata.internal.Dependencies;
- import org.apache.sis.xml.util.LegacyNamespaces;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
  import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.quality.TemporalQuality;
 -import org.opengis.metadata.quality.EvaluationMethod;
 -import org.opengis.metadata.quality.MeasureReference;
 -import org.opengis.metadata.quality.Metaquality;
 +// Specific to the main branch:
 +import org.opengis.metadata.quality.TemporalAccuracy;
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.UNSPECIFIED;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultCoverageResult.java
index 2759dada28,b69dadac2d..89eb5cfd4e
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultCoverageResult.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultCoverageResult.java
@@@ -28,13 -28,8 +28,13 @@@ import org.opengis.metadata.distributio
  import org.opengis.metadata.spatial.SpatialRepresentation;
  import org.opengis.metadata.spatial.SpatialRepresentationType;
  import org.apache.sis.xml.bind.FilterByVersion;
- import org.apache.sis.xml.util.LegacyNamespaces;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.CONDITIONAL;
 +import static org.opengis.annotation.Specification.UNSPECIFIED;
 +
  
  /**
   * Result of a data quality measure organising the measured values as a coverage.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java
index 010af2207c,2d5f6c2e8f..4be27a275a
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java
@@@ -34,12 -34,12 +34,12 @@@ import org.apache.sis.util.privy.CloneA
  import org.apache.sis.util.collection.CheckedContainer;
  import org.apache.sis.util.resources.Errors;
  import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
- import static org.apache.sis.metadata.internal.ImplementationHelper.valueIfDefined;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.valueIfDefined;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.quality.EvaluationMethod;
 -import org.opengis.metadata.quality.DataEvaluation;
 -import org.opengis.metadata.quality.AggregationDerivation;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.UNSPECIFIED;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultMeasureReference.java
index 884e697ec9,f540568839..924fa1f2a9
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultMeasureReference.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/quality/DefaultMeasureReference.java
@@@ -23,13 -23,10 +23,13 @@@ import jakarta.xml.bind.annotation.XmlR
  import org.opengis.util.InternationalString;
  import org.opengis.metadata.Identifier;
  import org.opengis.metadata.quality.Element;
- import org.apache.sis.util.internal.CollectionsExt;
+ import org.apache.sis.util.privy.CollectionsExt;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.quality.MeasureReference;
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Obligation.CONDITIONAL;
 +import static org.opengis.annotation.Specification.UNSPECIFIED;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/spatial/DefaultDimension.java
index 4e6eb3d37a,662f68eeda..041c08bd8e
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/spatial/DefaultDimension.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/spatial/DefaultDimension.java
@@@ -30,13 -30,8 +30,13 @@@ import org.apache.sis.metadata.TitlePro
  import org.apache.sis.metadata.iso.ISOMetadata;
  import org.apache.sis.measure.ValueRange;
  import org.apache.sis.util.ArgumentChecks;
- import static org.apache.sis.metadata.internal.ImplementationHelper.ensurePositive;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.ensurePositive;
  
 +// Specific to the main branch:
 +import org.opengis.annotation.UML;
 +import static org.opengis.annotation.Obligation.OPTIONAL;
 +import static org.opengis.annotation.Specification.ISO_19115;
 +
  
  /**
   * Axis properties.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
index 0000000000,13a42017ea..d42c6cadd4
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
@@@ -1,0 -1,299 +1,306 @@@
+ /*
+  * 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.metadata.privy;
+ 
+ import java.util.Locale;
+ import org.opengis.metadata.Identifier;
+ import org.opengis.metadata.citation.Citation;
+ import org.opengis.util.InternationalString;
+ import org.apache.sis.util.CharSequences;
+ import org.apache.sis.util.Characters;
+ import org.apache.sis.util.Deprecable;
+ import org.apache.sis.util.Static;
+ import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.privy.CollectionsExt;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.xml.NilObject;
+ import org.apache.sis.xml.NilReason;
+ 
++// Specific to the main branch:
++import org.opengis.referencing.ReferenceIdentifier;
++
+ 
+ /**
+  * Methods working on {@link Identifier} instances.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class Identifiers extends Static {
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private Identifiers() {
+     }
+ 
+     /**
+      * Returns {@code true} if the given code is {@code "EPSG"} while the codespace is {@code "IOGP"} or {@code "OGP"}
+      * (ignoring case). This particular combination of code and codespace is handled in a special way.
+      *
+      * <p>This method can be used for identifying where in Apache SIS source code the relationship between
+      * EPSG authority and IOGP code space is hard-coded.</p>
+      *
+      * @param  codeSpace  the identifier code space, or {@code null}.
+      * @param  code       the identifier code, or {@code null}.
+      * @return {@code true} if the given identifier is {@code "IOGP:EPSG"}.
+      *
+      * @see org.apache.sis.metadata.iso.citation.Citations#EPSG
+      */
+     public static boolean isEPSG(final String codeSpace, final String code) {
+         return Constants.EPSG.equalsIgnoreCase(code) &&
+               (Constants.IOGP.equalsIgnoreCase(codeSpace) || "OGP".equalsIgnoreCase(codeSpace) ||
+                Constants.EPSG.equalsIgnoreCase(codeSpace));
+         // "OGP" is a legacy abbreviation that existed before "IOGP".
+     }
+ 
+     /**
+      * Return {@code true} if the given object is deprecated.
+      */
+     private static boolean isDeprecated(final Object object) {
+         return (object instanceof Deprecable) && ((Deprecable) object).isDeprecated();
+     }
+ 
+     /**
+      * Returns a "unlocalized" string representation of the given international string, or {@code null} if none
+      * or if the string is deprecated or empty. This method is used by {@link #getIdentifier(Citation, boolean)},
+      * which is why we don't want the localized string.
+      */
+     private static String trimOrNull(final InternationalString title) {
+         return (title != null && !isDeprecated(title)) ? Strings.trimOrNull(title.toString(Locale.ROOT)) : null;
+     }
+ 
+     /**
+      * Infers an identifier from the given citation, or returns {@code null} if no identifier has been found.
+      * This method removes leading and trailing {@linkplain Character#isWhitespace(int) whitespaces}.
+      * See {@link Citations#getIdentifier(Citation)} for the public documentation of this method.
+      *
+      * <h4>Which method to use</h4>
+      * Guidelines:
+      * <ul>
+      *   <li>For information purpose (e.g. some {@code toString()} methods), use {@code getIdentifier(…, false)}.</li>
+      *   <li>For WKT formatting, use {@code getIdentifier(…, true)} in order to preserve formatting characters.</li>
+      *   <li>For assigning a value to a {@code codeSpace} field, use {@link Citations#toCodeSpace(Citation)}.</li>
+      * </ul>
+      *
+      * Use {@code toCodeSpace(…)} method when assigning values to be returned by methods like
 -     * {@link Identifier#getCodeSpace()}, since those values are likely to be compared without special
++     * {@link ReferenceIdentifier#getCodeSpace()}, since those values are likely to be compared without special
+      * care about ignorable identifier characters. But if the intent is to format a more complex string
+      * like WKT or {@code toString()}, then we suggest to use {@code getIdentifier(citation, true)} instead,
+      * which will produce the same result but preserving the ignorable characters, which can be useful
+      * for formatting purpose.
+      *
+      * @param  citation  the citation for which to get the identifier, or {@code null}.
+      * @param  strict    {@code true} for returning a non-null value only if the identifier is a valid Unicode identifier.
+      * @return a non-empty identifier for the given citation without leading or trailing whitespaces,
+      *         or {@code null} if the given citation is null or does not declare any identifier or title.
+      *
+      * @see <a href="https://issues.apache.org/jira/browse/SIS-201">SIS-201</a>
+      */
+     public static String getIdentifier(final Citation citation, final boolean strict) {
+         if (citation != null) {
+             boolean isUnicode = false;      // Whether `identifier` is a Unicode identifier.
+             String identifier = null;       // The best identifier found so far.
+             String codeSpace  = null;       // Code space of the identifier, or null if none.
+             for (final Identifier id : CollectionsExt.nonNull(citation.getIdentifiers())) {
+                 if (id != null && !isDeprecated(id)) {
+                     final String candidate = Strings.trimOrNull(id.getCode());
+                     if (candidate != null) {
+                         /*
+                          * For a non-empty identifier, verify if both the code and its codespace are valid
+                          * Unicode identifiers. If a codespace exists, then the code does not need to begin
+                          * with a "Unicode identifier start" (it may be a "Unicode identifier part").
+                          */
 -                        final String cs = Strings.trimOrNull(id.getCodeSpace());
 -                        if (cs == null) {
++                        String cs = null;
++                        if (id instanceof ReferenceIdentifier) {
++                            cs = Strings.trimOrNull(((ReferenceIdentifier) id).getCodeSpace());
++                        }
++                        if (cs == null || cs.isEmpty()) {
++                            cs = null;
+                             isUnicode = CharSequences.isUnicodeIdentifier(candidate);
+                         } else {
+                             isUnicode = CharSequences.isUnicodeIdentifier(cs);
+                             if (isUnicode) for (int i = 0; i < candidate.length();) {
+                                 final int c = candidate.codePointAt(i);
+                                 if (!Character.isUnicodeIdentifierPart(c) &&
+                                         (strict || (c != '.' && c != '-')))
+                                 {
+                                     /*
+                                      * Above special case for '.' and '-' characters is documented
+                                      * in the public Citations.getIdentifier(Citation) method.
+                                      */
+                                     isUnicode = false;
+                                     break;
+                                 }
+                                 i += Character.charCount(c);
+                             }
+                         }
+                         /*
+                          * If we found a Unicode identifier, we are done and we can exit the loop.
+                          * Otherwise retain the first identifier and continue the search for Unicode identifier.
+                          */
+                         if (identifier == null || isUnicode) {
+                             identifier = candidate;
+                             codeSpace  = cs;
+                             if (isUnicode) break;
+                         }
+                     }
+                 }
+             }
+             /*
+              * If no identifier has been found, fallback on the first title or alternate title.
+              * We search for alternate titles because ISO specification said that those titles
+              * are often used for abbreviations. Again we give preference to Unicode identifiers,
+              * which are typically alternate titles.
+              */
+             if (identifier == null) {
+                 identifier = trimOrNull(citation.getTitle());       // Whitepaces removed by trimOrNull(…).
+                 if (identifier != null) {
+                     isUnicode = CharSequences.isUnicodeIdentifier(identifier);
+                 }
+                 if (!isUnicode) {
+                     for (final InternationalString i18n : CollectionsExt.nonNull(citation.getAlternateTitles())) {
+                         final String candidate = trimOrNull(i18n);
+                         if (candidate != null) {
+                             isUnicode = CharSequences.isUnicodeIdentifier(candidate);
+                             if (identifier == null || isUnicode) {
+                                 identifier = candidate;
+                                 if (isUnicode) break;
+                             }
+                         }
+                     }
+                 }
+             }
+             /*
+              * Finished searching in the identifiers, title and alternate titles. If the identifier that
+              * we found is not a valid Unicode identifier, we will return it only if the caller did not
+              * asked for strictly valid Unicode identifier.
+              */
+             if (isUnicode || !strict) {
+                 if (codeSpace != null && !isEPSG(codeSpace, identifier)) {
+                     return codeSpace + (strict ? '_' : Constants.DEFAULT_SEPARATOR) + identifier;
+                 } else {
+                     return identifier;
+                 }
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns {@code true} if the given identifier authority matches the given {@code authority}.
+      * If one of the authority is null, then the comparison fallback on the given {@code codeSpace}.
+      * If the code spaces are also null, then this method conservatively returns {@code false}.
+      *
+      * @param  identifier  the identifier to compare.
+      * @param  authority   the desired authority, or {@code null}.
+      * @param  codeSpace   the desired code space or {@code null}, used as a fallback if an authority is null.
+      * @return {@code true} if the authority or code space (as a fallback only) matches.
+      */
+     private static boolean authorityMatches(final Identifier identifier, final Citation authority, final String codeSpace) {
+         if (authority != null) {
+             final Citation other = identifier.getAuthority();
+             if (other != null) {
+                 return Citations.identifierMatches(authority, other);
+             }
+         }
 -        if (codeSpace != null) {
 -            final String other = identifier.getCodeSpace();
++        if (codeSpace != null && identifier instanceof ReferenceIdentifier) {
++            final String other = ((ReferenceIdentifier) identifier).getCodeSpace();
+             if (other != null) {
+                 return CharSequences.equalsFiltered(codeSpace, other, Characters.Filter.UNICODE_IDENTIFIER, true);
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Determines whether a match or mismatch is found between the two given collections of identifiers.
+      * If any of the given collections is {@code null} or empty, then this method returns {@code null}.
+      *
+      * <p>According ISO 19162 (<cite>Well known text representation of coordinate reference systems</cite>),
+      * {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getIdentifiers() identifiers} should have precedence over
+      * {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getName() name} for identifying {@code IdentifiedObject}s,
+      * at least in the case of {@linkplain org.apache.sis.referencing.operation.DefaultOperationMethod operation methods} and
+      * {@linkplain org.apache.sis.parameter.AbstractParameterDescriptor parameters}.</p>
+      *
+      * @param  id1  the first collection of identifiers, or {@code null}.
+      * @param  id2  the second collection of identifiers, or {@code null}.
+      * @return {@code TRUE} or {@code FALSE} on match or mismatch respectively, or {@code null} if this method
+      *         cannot determine if there is a match or mismatch.
+      */
 -    public static Boolean hasCommonIdentifier(final Iterable<? extends Identifier> id1,
 -                                              final Iterable<? extends Identifier> id2)
++    public static Boolean hasCommonIdentifier(final Iterable<? extends ReferenceIdentifier> id1,
++                                              final Iterable<? extends ReferenceIdentifier> id2)
+     {
+         if (id1 != null && id2 != null) {
+             boolean hasFound = false;
 -            for (final Identifier identifier : id1) {
++            for (final ReferenceIdentifier identifier : id1) {
+                 final Citation authority = identifier.getAuthority();
+                 final String   codeSpace = identifier.getCodeSpace();
+                 for (final Identifier other : id2) {
+                     if (authorityMatches(identifier, authority, codeSpace)) {
+                         if (CharSequences.equalsFiltered(identifier.getCode(), other.getCode(), Characters.Filter.UNICODE_IDENTIFIER, true)) {
+                             return Boolean.TRUE;
+                         }
+                         hasFound = true;
+                     }
+                 }
+             }
+             if (hasFound) {
+                 return Boolean.FALSE;
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns a message saying that a property is missing for an object having the given identifier.
+      *
+      * @param  owner     identifier of the object for which a property is missing.
+      * @param  property  name of the missing property.
+      * @return a message saying that a value is missing for the given property in the specified identified object.
+      */
+     public static String missingValueForProperty(final Identifier owner, final String property) {
+         return (owner == null)
+                 ? Errors.format(Errors.Keys.MissingValueForProperty_1, property)
+                 : Errors.format(Errors.Keys.MissingValueForProperty_2, owner, property);
+     }
+ 
+     /**
+      * Returns a string representation of the reason why an object is nil.
+      * If the object provides a {@link NilReason}, then its string representation is returned.
+      * Otherwise, if the given object provides identifiers, then they are assumed unresolved references.
+      * Otherwise, this method returns "unspecified" in the default locale.
+      *
+      * @param  object  the object for which to get the nil reason.
+      * @return string representation of the nil reason.
+      */
+     public static String getNilReason(final NilObject object) {
+         NilReason reason = object.getNilReason();
+         if (reason != null) {
+             return reason.toString();
+         }
+         if (object instanceof org.apache.sis.xml.IdentifiedObject) {
+             for (String id : ((org.apache.sis.xml.IdentifiedObject) object).getIdentifierMap().values()) {
+                 if (id != null && !id.isBlank()) {
+                     return "unresolved " + id;
+                 }
+             }
+         }
+         return Vocabulary.format(Vocabulary.Keys.Unspecified);
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
index 0000000000,1fe8975835..47c3f215fc
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
@@@ -1,0 -1,313 +1,321 @@@
+ /*
+  * 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.metadata.privy;
+ 
+ import java.util.Locale;
+ import java.util.Objects;
+ import java.util.Collection;
+ import org.opengis.util.NameSpace;
+ import org.opengis.util.GenericName;
+ import org.opengis.util.InternationalString;
+ import org.opengis.referencing.IdentifiedObject;
+ import org.opengis.metadata.Identifier;
+ import org.opengis.metadata.citation.Citation;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.util.CharSequences;
+ import org.apache.sis.util.iso.DefaultNameSpace;
+ import static org.apache.sis.util.Characters.Filter.LETTERS_AND_DIGITS;
+ 
+ // Specific to the main and geoapi-3.1 branches:
+ import org.opengis.referencing.ReferenceIdentifier;
+ 
+ 
+ /**
+  * Does the unobvious mapping between {@link Identifier} properties and {@link GenericName} ones.
+  * This class also implements the {@link #isHeuristicMatchForName(Identifier, Collection, CharSequence, Simplifier)}
+  * method since that method involves a mix of names and identifiers.
+  *
+  * <h2>Limitations</h2>
+  * Current version does not yet work with URN or HTTP syntax.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class NameToIdentifier implements ReferenceIdentifier {
+     /**
+      * The name from which to infer the identifier attributes.
+      */
+     private final GenericName name;
+ 
+     /**
+      * Infers the attributes from the given name.
+      *
+      * @param  name  the name from which to infer the identifier properties.
+      */
+     public NameToIdentifier(final GenericName name) {
+         this.name = Objects.requireNonNull(name);
+     }
+ 
+     /**
+      * Returns the scope of the given name if it is not global.
+      * This method is null-safe, including paranoiac checks against null scope.
+      *
+      * @param  name  the name from which to get the scope, or {@code null}.
+      * @return the scope of the given name, or {@code null} if the given name was null or has a global scope.
+      */
+     private static GenericName scope(final GenericName name) {
+         if (name != null) {
+             final NameSpace scope = name.scope();
+             if (scope != null && !scope.isGlobal()) {
+                 return scope.name();
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Infers the authority from the scope if any, or from the code space otherwise.
+      *
+      * @return the authority, or {@code null} if none.
+      */
+     @Override
+     public Citation getAuthority() {
+         GenericName scope = scope(name);
+         if (scope == null) {
+             scope = scope(name.tip());
+             if (scope == null) {
+                 return null;
+             }
+         }
+         return Citations.fromName(scope.head().toString());
+     }
+ 
+     /**
+      * Takes the element before the tip as the code space.
+      *
+      * @param  name    the name from which to get the code space, or {@code null}.
+      * @param  locale  the locale, or {@code null} for a call to {@code name.toString()}.
+      * @return the code space, or {@code null} if none.
+      */
+     public static String getCodeSpace(final GenericName name, final Locale locale) {
+         final GenericName scope = scope(name.tip());
+         return (scope != null) ? toString(scope.tip(), locale) : null;
+     }
+ 
+     /**
+      * Takes everything except the tip as the code space.
+      */
+     @Override
+     public String getCodeSpace() {
+         return getCodeSpace(name, null);
+     }
+ 
+     /**
+      * Takes the last element as the code.
+      */
+     @Override
+     public String getCode() {
+         return name.tip().toString();
+     }
+ 
++    /**
++     * Returns {@code null} since names are not versioned.
++     */
++    @Override
++    public String getVersion() {
++        return null;
++    }
++
+     /**
+      * Returns a hash code value for this object.
+      */
+     @Override
+     public int hashCode() {
+         return ~Objects.hashCode(name);
+     }
+ 
+     /**
+      * Compares this object with the given one for equality.
+      *
+      * @param  object  the object to compare with this identifier.
+      * @return {@code true} if both objects are equal.
+      */
+     @Override
+     public boolean equals(final Object object) {
+         if (object == this) {
+             return true;
+         }
+         if (object != null && object.getClass() == getClass()) {
+             return Objects.equals(name, ((NameToIdentifier) object).name);
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns the string representation of this identifier.
+      *
+      * @return the string representation of this identifier.
+      */
+     @Override
+     public String toString() {
+         final String code = getCode();
+         final String cs = getCodeSpace();
+         if (cs != null && !cs.isEmpty()) {
+             return cs + DefaultNameSpace.DEFAULT_SEPARATOR + code;
+         }
+         return code;
+     }
+ 
+     /**
+      * Returns a string representation of the given name in the given locale, with paranoiac checks against null value.
+      * Such null values should never happen since the properties used here are mandatory, but we try to make this class
+      * robust to broken implementations.
+      *
+      * @param  name    the name from which to get the localized string, or {@code null}.
+      * @param  locale  the locale, or {@code null} for a call to {@code name.toString()}.
+      * @return the localized string representation, or {@code null} if the given name was null.
+      */
+     public static String toString(final GenericName name, final Locale locale) {
+         if (name != null) {
+             if (locale != null) {
+                 final InternationalString i18n = name.toInternationalString();
+                 if (i18n != null) {
+                     final String s = i18n.toString(locale);
+                     if (s != null) {
+                         return s;
+                     }
+                 }
+             }
+             return name.toString();
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns {@code true} if the given identifier to search matches one of the object identifiers.
+      *
+      * @param  identifiers  the identifiers to compare against {@code toSearch}.
+      * @param  toSearch     the identifier to check for equality.
+      * @return {@code true} if the identifier to search is found in the given set of identifiers.
+      */
 -    public static boolean isHeuristicMatchForIdentifier(final Iterable<? extends Identifier> identifiers, final String toSearch) {
++    public static boolean isHeuristicMatchForIdentifier(final Iterable<? extends ReferenceIdentifier> identifiers, final String toSearch) {
+         if (toSearch != null && identifiers != null) {
+             int s = toSearch.indexOf(DefaultNameSpace.DEFAULT_SEPARATOR);
+             if (s < 0) {
+                 // no codespace in searched name
+                 for (final Identifier id : identifiers) {
+                     if (toSearch.equalsIgnoreCase(id.getCode())) {
+                         return true;
+                     }
+                 }
+                 return false;
+             }
+             do {
+                 final String codespace = toSearch.substring(0, s).trim();
+                 final String code = toSearch.substring(++s).trim();
 -                for (final Identifier id : identifiers) {
++                for (final ReferenceIdentifier id : identifiers) {
+                     if (codespace.equalsIgnoreCase(id.getCodeSpace()) && code.equalsIgnoreCase(id.getCode())) {
+                         return true;
+                     }
+                 }
+                 s = toSearch.indexOf(DefaultNameSpace.DEFAULT_SEPARATOR, s);
+             } while (s >= 0);
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns {@code true} if the given {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getName()
+      * primary name} or one of the given aliases matches the given name. The comparison ignores case, some Latin
+      * diacritical signs and any characters that are not letters or digits.
+      *
+      * @param  name        the name of the {@code IdentifiedObject} to check.
+      * @param  aliases     the list of aliases in the {@code IdentifiedObject} (may be {@code null}).
+      *                     This method will never modify that list, so the given list can be a direct
+      *                     reference to an internal list.
+      * @param  toSearch    the name for which to check for equality.
+      * @param  simplifier  a function for simplifying the names before comparison.
+      * @return {@code true} if the primary name or at least one alias matches the given {@code name}.
+      */
+     public static boolean isHeuristicMatchForName(final Identifier name, final Collection<GenericName> aliases,
+             CharSequence toSearch, final Simplifier simplifier)
+     {
+         if (toSearch != null) {
+             CharSequence code = (name != null) ? name.getCode() : null;
+             if (toSearch.equals(code)) {
+                 return true;                                                    // Optimization for a common case.
+             }
+             toSearch = simplifier.apply(toSearch);
+             code     = simplifier.apply(code);
+             if (CharSequences.equalsFiltered(toSearch, code, LETTERS_AND_DIGITS, true)) {
+                 return true;
+             }
+             if (aliases != null) {
+                 for (final GenericName alias : aliases) {
+                     if (alias != null) {                                                        // Paranoiac check.
+                         final CharSequence tip = simplifier.apply(alias.tip().toString());
+                         if (CharSequences.equalsFiltered(toSearch, tip, LETTERS_AND_DIGITS, true)) {
+                             return true;
+                         }
+                         /*
+                          * Note: a previous version compared also the scoped names. We removed that part,
+                          * because experience has shown that this method is used only for the "code" part
+                          * of an object name. If we really want to compare scoped name, it would probably
+                          * be better to take a GenericName argument instead of String.
+                          */
+                     }
+                 }
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns {@code true} if the {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getName()
+      * primary name} or an aliases of the given object matches the given name. The comparison ignores case,
+      * some Latin diacritical signs and any characters that are not letters or digits.
+      *
+      * @param  object  the object for which to check the name or alias.
+      * @param  name    the name to compare with the object name or aliases.
+      * @return {@code true} if the primary name of at least one alias matches the specified {@code name}.
+      */
+     public static boolean isHeuristicMatchForName(final IdentifiedObject object, final String name) {
+         return isHeuristicMatchForName(object.getName(), object.getAlias(), name, Simplifier.DEFAULT);
+     }
+ 
+     /**
+      * A function for simplifying an {@link org.opengis.referencing.IdentifiedObject} name before comparison with
+      * {@link NameToIdentifier#isHeuristicMatchForName(Identifier, Collection, CharSequence, Simplifier)}.
+      */
+     public static class Simplifier {
+         /**
+          * The prefix used by ESRI at the beginning of datum names.
+          * This prefix should be omitted when simplifying a datum name.
+          */
+         public static final String ESRI_DATUM_PREFIX = "D_";
+ 
+         /**
+          * The default instance, which replaces some non-ASCII characters by ASCII ones.
+          */
+         public static final Simplifier DEFAULT = new Simplifier();
+ 
+         /**
+          * For subclasses and default instance only.
+          */
+         protected Simplifier() {
+         }
+ 
+         /**
+          * Simplifies the given name.
+          *
+          * @param  name  the object name (may be {@code null}).
+          * @return the name to use for comparison purpose, or {@code null}.
+          */
+         protected CharSequence apply(final CharSequence name) {
+             return CharSequences.toASCII(name);
+         }
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ReferencingServices.java
index 0000000000,eed74e2426..c71336c2b0
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ReferencingServices.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/ReferencingServices.java
@@@ -1,0 -1,301 +1,314 @@@
+ /*
+  * 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.metadata.privy;
+ 
+ import java.util.Locale;
+ import java.util.TimeZone;
+ import java.util.ServiceLoader;
+ import java.text.Format;
+ import org.opengis.geometry.Envelope;
+ import org.opengis.geometry.DirectPosition;
+ import org.opengis.parameter.ParameterDescriptor;
+ import org.opengis.referencing.IdentifiedObject;
+ import org.opengis.referencing.operation.CoordinateOperationFactory;
+ import org.opengis.referencing.operation.TransformException;
+ import org.opengis.util.FactoryException;
+ import org.apache.sis.metadata.iso.extent.DefaultExtent;
+ import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent;
+ import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent;
+ import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+ import org.apache.sis.metadata.iso.extent.DefaultSpatialTemporalExtent;
+ import org.apache.sis.system.OptionalDependency;
+ import org.apache.sis.system.Modules;
+ 
++// Specific to the main branch:
++import org.opengis.util.TypeName;
++
+ 
+ /**
+  * Provides access to services defined in the {@code org.apache.sis.referencing} module.
+  * This class also opportunistically defines some methods and constants related
+  * to <q>referencing by coordinates</q> but needed by metadata.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public class ReferencingServices extends OptionalDependency {
+     /**
+      * The length of one nautical mile, which is {@value} metres.
+      */
+     public static final double NAUTICAL_MILE = 1852;
+ 
+     /**
+      * The GRS80 {@linkplain org.apache.sis.referencing.datum.DefaultEllipsoid#getAuthalicRadius() authalic radius},
+      * which is {@value} metres. This is close to the WGS84 authalic radius, which is about 6371007.180918474 when
+      * computed with {@code double} precision.
+      */
+     public static final double AUTHALIC_RADIUS = 6371007;
+ 
+     /**
+      * The services, fetched when first needed.
+      * Guaranteed non-null after the search was done, even if the service implementation was not found.
+      *
+      * @see #getInstance()
+      */
+     private static volatile ReferencingServices instance;
+ 
+     /**
+      * For subclass only. This constructor registers this instance as a
+      * {@link org.apache.sis.system.SystemListener} in order to
+      * force a new {@code ReferencingServices} lookup if the module path changes.
+      */
+     protected ReferencingServices() {
+         super(Modules.METADATA, Modules.REFERENCING);
+     }
+ 
+     /**
+      * Invoked when the module path changed. Resets the {@link #instance} to {@code null}
+      * in order to force the next call to {@link #getInstance()} to fetch a new one,
+      * which may be different.
+      */
+     @Override
+     protected final void classpathChanged() {
+         synchronized (ReferencingServices.class) {
+             instance = null;
+         }
+     }
+ 
+     /**
+      * Returns the singleton instance.
+      *
+      * @return the singleton instance.
+      */
+     @SuppressWarnings("DoubleCheckedLocking")
+     public static ReferencingServices getInstance() {
+         ReferencingServices c = instance;
+         if (c == null) {
+             synchronized (ReferencingServices.class) {
+                 c = instance;
+                 if (c == null) {
+                     /*
+                      * Double-checked locking: okay since Java 5 provided that the `instance` field is volatile.
+                      * In the particular case of this class, the intent is to ensure that SystemListener.add(…)
+                      * is invoked only once.
+                      */
+                     c = getInstance(ReferencingServices.class, ServiceLoader.load(ReferencingServices.class), Modules.REFERENCING);
+                     if (c == null) {
+                         c = new ReferencingServices();
+                     }
+                     instance = c;
+                 }
+             }
+         }
+         return c;
+     }
+ 
+ 
+ 
+ 
+     ///////////////////////////////////////////////////////////////////////////////////////
+     ////                                                                               ////
+     ////                        SERVICES FOR ISO 19115 METADATA                        ////
+     ////                                                                               ////
+     ///////////////////////////////////////////////////////////////////////////////////////
+ 
+     /**
+      * Sets a geographic bounding box from the specified envelope.
+      * If the envelope contains a CRS which is not geographic, then the bounding box will be transformed
+      * to a geographic CRS (without datum shift if possible). Otherwise, the envelope is assumed already
+      * in a geographic CRS using (<var>longitude</var>, <var>latitude</var>) axis order.
+      *
+      * <p>If {@code findOpCaller} is non-null, then this method is invoked for computing a <em>hint</em>
+      * for choosing a coordinate operation between a pair of reference systems. It changes the behavior
+      * of this methods in two ways:</p>
+      *
+      * <ul>
+      *   <li>Some failures will cause this method to return {@code null} instead of throwing an exception.
+      *       Those exception will be logged on the assumption that {@code findOpCaller} is a public method
+      *       of {@code Envelopes}.</li>
+      *   <li>The bounding box may be conservatively expanded to the whole world.</li>
+      * </ul>
+      *
+      * @param  envelope      the source envelope.
+      * @param  target        the target bounding box, or {@code null} for creating it automatically.
+      * @param  findOpCaller  non-null for computing a hint rather than an exact bounding box.
+      * @return the bounding box, or {@code null} on failure (in hint mode) or if no horizontal component was found.
+      * @throws UnsupportedOperationException if the {@code org.apache.sis.referencing} module has not been found on the module path.
+      * @throws TransformException if the given envelope cannot be transformed.
+      */
+     public DefaultGeographicBoundingBox setBounds(Envelope envelope, DefaultGeographicBoundingBox target, String findOpCaller)
+             throws TransformException
+     {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Sets a vertical extent with the value inferred from the given envelope.
+      * Only the vertical coordinates are extracted; all other coordinates are ignored.
+      *
+      * @param  envelope  the source envelope.
+      * @param  target    the target vertical extent.
+      * @return whether the envelope contains a vertical component.
+      * @throws UnsupportedOperationException if the {@code org.apache.sis.referencing} module has not been found on the module path.
+      */
+     public boolean setBounds(Envelope envelope, DefaultVerticalExtent target) {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Sets a temporal extent with the value inferred from the given envelope.
+      * Only the temporal coordinates are extracted; all other coordinates are ignored.
+      *
+      * @param  envelope  the source envelope.
+      * @param  target    the target temporal extent.
+      * @return whether the envelope contains a temporal component.
+      * @throws UnsupportedOperationException if the {@code org.apache.sis.referencing} module has not been found on the module path.
+      */
+     public boolean setBounds(Envelope envelope, DefaultTemporalExtent target) {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Sets the geographic, vertical and temporal extents with the values inferred from the given envelope.
+      * If the given {@code target} has more geographic or vertical extents than needed (0 or 1), then the
+      * extraneous extents are removed.
+      *
+      * <p>Behavior regarding missing dimensions:</p>
+      * <ul>
+      *   <li>If the given envelope has no horizontal component, then all geographic extents are removed
+      *       from the given {@code target}. Non-geographic extents (e.g. descriptions and polygons) are
+      *       left unchanged.</li>
+      *   <li>If the given envelope has no vertical component, then the vertical extent is set to {@code null}.</li>
+      *   <li>If the given envelope has no temporal component, then the temporal extent is set to {@code null}.</li>
+      * </ul>
+      *
+      * @param  envelope  the source envelope.
+      * @param  target    the target spatiotemporal extent.
+      * @return whether the envelope contains a spatial or temporal component.
+      * @throws TransformException if a coordinate transformation was required and failed.
+      * @throws UnsupportedOperationException if the {@code org.apache.sis.referencing} module has not been found on the module path.
+      */
+     public boolean setBounds(Envelope envelope, DefaultSpatialTemporalExtent target) throws TransformException {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Initializes a horizontal, vertical and temporal extent with the values inferred from the given envelope.
+      *
+      * @param  envelope  the source envelope.
+      * @param  target    the target extent.
+      * @return whether the envelope contains a spatial or temporal component.
+      * @throws TransformException if a coordinate transformation was required and failed.
+      * @throws UnsupportedOperationException if the {@code org.apache.sis.referencing} module has not been found on the module path.
+      */
+     public boolean addElements(Envelope envelope, DefaultExtent target) throws TransformException {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Creates a two-dimensional geographic position associated to the default geographic CRS.
+      * Axis order is (longitude, latitude).
+      *
+      * @param  λ  the longitude value.
+      * @param  φ  the latitude value.
+      * @return the direct position for the given geographic coordinate.
+      */
+     public DirectPosition geographic(final double λ, final double φ) {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Returns an identifier for the given object, giving precedence to EPSG identifier if available.
+      * The returned string should be of the form {@code "AUTHORITY:CODE"} if possible (no guarantees).
+      *
+      * @param  object  the object for which to get an identifier.
+      * @return an identifier for the given object, with preference given to EPSG codes.
+      * @throws FactoryException if an error occurred while searching for the EPSG code.
+      */
+     public String getPreferredIdentifier(final IdentifiedObject object) throws FactoryException {
+         throw moduleNotFound();
+     }
+ 
+ 
+ 
+ 
+     ///////////////////////////////////////////////////////////////////////////////////////
+     ////                                                                               ////
+     ////                          OTHER REFERENCING SERVICES                           ////
+     ////                                                                               ////
+     ///////////////////////////////////////////////////////////////////////////////////////
+ 
++    /**
++     * Returns the name of the type of values.
++     *
++     * @param  parameter  parameter for which to get the name of type of values, or {@code null}.
++     * @return name of type of values, or {@code null} if not supported by given implementation.
++     */
++    public TypeName getValueType(ParameterDescriptor<?> parameter) {
++        return null;
++    }
++
+     /**
+      * Returns a fully implemented parameter descriptor.
+      *
+      * @param  <T>        the type of values.
+      * @param  parameter  a partially implemented parameter descriptor, or {@code null}.
+      * @return a fully implemented parameter descriptor, or {@code null} if the given argument was null.
+      * @throws UnsupportedOperationException if the {@code org.apache.sis.referencing} module has not been found on the module path.
+      */
+     public <T> ParameterDescriptor<T> toImplementation(ParameterDescriptor<T> parameter) {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Creates a format for {@link DirectPosition} instances.
+      *
+      * @param  locale    the locale for the new {@code Format}, or {@code null} for {@code Locale.ROOT}.
+      * @param  timezone  the timezone, or {@code null} for UTC.
+      * @return a {@link org.apache.sis.geometry.CoordinateFormat}.
+      */
+     public Format createCoordinateFormat(final Locale locale, final TimeZone timezone) {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Returns the default coordinate operation factory.
+      *
+      * @return the coordinate operation factory to use.
+      */
+     public CoordinateOperationFactory getCoordinateOperationFactory() {
+         throw moduleNotFound();
+     }
+ 
+     /**
+      * Returns information about the Apache SIS configuration to be reported in {@link org.apache.sis.setup.About}.
+      * This method is invoked only for aspects that depends on other modules than {@code org.apache.sis.util}.
+      *
+      * <p>Current keys are:</p>
+      * <ul>
+      *   <li>{@code "EPSG"}: version of EPSG database.</li>
+      * </ul>
+      *
+      * @param  key     a key identifying the information to return.
+      * @param  locale  language to use if possible.
+      * @return the information, or {@code null} if none.
+      *
+      * @see org.apache.sis.util.privy.MetadataServices#getInformation(String, Locale)
+      */
+     public String getInformation(String key, Locale locale) {
+         return null;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/TemporalUtilities.java
index 0000000000,8274143c87..3adf0d4cd1
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/TemporalUtilities.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/TemporalUtilities.java
@@@ -1,0 -1,144 +1,144 @@@
+ /*
+  * 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.metadata.privy;
+ 
+ import java.util.Date;
+ import java.util.ServiceLoader;
+ import org.opengis.temporal.TemporalPrimitive;
+ import org.apache.sis.system.Modules;
+ import org.apache.sis.system.Reflect;
+ import org.apache.sis.system.SystemListener;
+ import org.apache.sis.pending.temporal.DefaultTemporalFactory;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.temporal.Instant;
 -import org.opengis.temporal.Period;
 -import org.opengis.temporal.TemporalFactory;
++// Specific to the main branch:
++import org.apache.sis.pending.geoapi.temporal.Instant;
++import org.apache.sis.pending.geoapi.temporal.Period;
++import org.apache.sis.pending.geoapi.temporal.TemporalFactory;
+ 
+ 
+ /**
+  * Utilities related to ISO 19108 objects. This class may disappear after we reviewed
+  * the GeoAPI-pending temporal interfaces.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @author  Guilhem Legal (Geomatys)
+  */
+ public final class TemporalUtilities extends SystemListener {
+     /**
+      * The default factory to use for implementations.
+      */
+     private static volatile TemporalFactory implementation;
+ 
+     static {
+         SystemListener.add(new TemporalUtilities());
+     }
+ 
+     /**
+      * For the singleton system listener only.
+      */
+     private TemporalUtilities() {
+         super(Modules.METADATA);
+     }
+ 
+     /**
+      * Discards the cached factory when the module path has changed.
+      */
+     @Override
+     protected void classpathChanged() {
+         implementation = null;
+     }
+ 
+     /**
+      * Returns a temporal factory, or a default implementation if none.
+      *
+      * @return the temporal factory.
+      */
+     public static TemporalFactory getTemporalFactory() {
+         TemporalFactory factory = implementation;
+         if (factory == null) {
+             factory = ServiceLoader.load(TemporalFactory.class, Reflect.getContextClassLoader())
+                     .findFirst().orElseGet(DefaultTemporalFactory::provider);
+             implementation = factory;
+         }
+         return factory;
+     }
+ 
+     /**
+      * Creates an instant for the given date using the given factory.
+      */
+     private static Instant createInstant(final TemporalFactory factory, final Date date) {
+         return factory.createInstant(date);
+     }
+ 
+     /**
+      * Creates an instant for the given date.
+      *
+      * @param  time  the date for which to create instant, or {@code null}.
+      * @return the instant, or {@code null} if the given time was null.
+      * @throws UnsupportedOperationException if the temporal factory is not available on the module path.
+      */
+     public static Instant createInstant(final Date time) throws UnsupportedOperationException {
+         return (time != null) ? createInstant(getTemporalFactory(), time) : null;
+     }
+ 
+     /**
+      * Creates a period for the given begin and end dates. The given arguments can be null if the
+      * {@link TemporalFactory#createInstant(Date)} method accepts null dates, which stand for
+      * undetermined position.
+      *
+      * @param  begin  the begin date, inclusive.
+      * @param  end    the end date, inclusive.
+      * @return the period.
+      * @throws UnsupportedOperationException if the temporal factory is not available on the module path.
+      */
+     public static Period createPeriod(final Date begin, final Date end) throws UnsupportedOperationException {
+         final TemporalFactory factory = getTemporalFactory();
+         return factory.createPeriod(createInstant(factory, begin), createInstant(factory, end));
+     }
+ 
+     /**
+      * Infers a value from the extent as a {@link Date} object.
+      * This method is used for compatibility with legacy API and may disappear in future SIS version.
+      *
+      * @param  time  the instant or period for which to get a date, or {@code null}.
+      * @return the requested time as a Java date, or {@code null} if none.
+      */
+     public static Date getDate(final TemporalPrimitive time) {
+         Instant instant;
+         if (time instanceof Instant) {
+             instant = (Instant) time;
+         } else if (time instanceof Period) {
+             instant = ((Period) time).getEnding();
+             if (instant == null) {
+                 instant = ((Period) time).getBeginning();
+             }
+         } else {
+             return null;
+         }
+         return instant.getDate();
+     }
+ 
+     /**
+      * Temporary method, to be removed after we upgraded metadata to {@link java.time}.
+      *
+      * @param  instant  the Java instant, or {@code null}.
+      * @return the legacy Java date, or {@code null}.
+      */
+     public static Date toDate(final java.time.Instant instant) {
+         return (instant != null) ? Date.from(instant) : null;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleCitation.java
index cb66428e0f,ef28b60e0b..32ffb58faa
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleCitation.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleCitation.java
@@@ -21,18 -21,8 +21,18 @@@ import java.io.Serializable
  import org.opengis.metadata.citation.Citation;
  import org.opengis.util.InternationalString;
  import org.apache.sis.util.SimpleInternationalString;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  
 +// Specific to the main branch:
 +import java.util.Date;
 +import java.util.Collection;
 +import java.util.Collections;
 +import org.opengis.metadata.Identifier;
 +import org.opengis.metadata.citation.CitationDate;
 +import org.opengis.metadata.citation.PresentationForm;
 +import org.opengis.metadata.citation.ResponsibleParty;
 +import org.opengis.metadata.citation.Series;
 +
  
  /**
   * A trivial implementation of {@link Citation} containing only a title.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleDuration.java
index dc2db3159b,ca7d4a9c7e..cc1b740ee4
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleDuration.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleDuration.java
@@@ -16,10 -16,10 +16,10 @@@
   */
  package org.apache.sis.metadata.simple;
  
- import org.apache.sis.util.internal.Numerics;
+ import org.apache.sis.util.privy.Numerics;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.temporal.Duration;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.temporal.Duration;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleExtent.java
index bf90988817,0c3f18d82d..195b46c59a
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleExtent.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleExtent.java
@@@ -21,11 -21,8 +21,11 @@@ import org.opengis.metadata.extent.Exte
  import org.opengis.metadata.extent.GeographicExtent;
  import org.opengis.metadata.extent.TemporalExtent;
  import org.opengis.metadata.extent.VerticalExtent;
- import static org.apache.sis.util.internal.CollectionsExt.singletonOrEmpty;
+ import static org.apache.sis.util.privy.CollectionsExt.singletonOrEmpty;
  
 +// Specific to the main branch:
 +import org.opengis.util.InternationalString;
 +
  
  /**
   * A trivial implementation of {@link Extent} containing only geographic, vertical and temporal extent.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleFormat.java
index 6a534aa5df,c60f33a316..5ac0d5a56a
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleFormat.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/simple/SimpleFormat.java
@@@ -20,10 -20,10 +20,10 @@@ import java.util.Collection
  import java.util.Collections;
  import org.opengis.util.InternationalString;
  import org.opengis.metadata.distribution.Format;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.citation.Citation;
 +// Specific to the main branch:
 +import org.opengis.metadata.distribution.Distributor;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
index 980084434d,54a7bc5a76..7c24697c1c
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
@@@ -46,15 -46,14 +46,15 @@@ import org.apache.sis.metadata.TypeValu
  import org.apache.sis.metadata.ValueExistencePolicy;
  import org.apache.sis.metadata.TitleProperty;
  import org.apache.sis.metadata.iso.citation.Citations;
- import org.apache.sis.metadata.sql.util.SQLBuilder;
- import org.apache.sis.metadata.sql.util.Reflection;
- import org.apache.sis.util.internal.Constants;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.metadata.sql.privy.SQLBuilder;
+ import org.apache.sis.metadata.sql.privy.Reflection;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.privy.Strings;
  import org.apache.sis.xml.IdentifiedObject;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.ControlledVocabulary;
 +// Specific to the main branch:
 +import org.opengis.util.CodeList;
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordSchema.java
index 2d9ec927c8,8b29bd616c..f556a041c2
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordSchema.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordSchema.java
@@@ -37,8 -37,11 +37,8 @@@ import org.apache.sis.util.resources.Er
  import org.apache.sis.util.collection.WeakValueHashMap;
  import org.apache.sis.metadata.simple.SimpleAttributeType;
  import org.apache.sis.converter.SurjectiveConverter;
- import org.apache.sis.util.internal.Strings;
+ import org.apache.sis.util.privy.Strings;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -
  
  /**
   * A collection of record types in a given namespace.
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/RecordDefinition.java
index e75e38fcdd,2cc12414c3..29ea7632df
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/RecordDefinition.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/RecordDefinition.java
@@@ -28,11 -28,11 +28,11 @@@ import org.opengis.util.MemberName
  import org.apache.sis.util.Classes;
  import org.apache.sis.util.Numbers;
  import org.apache.sis.util.CharSequences;
- import org.apache.sis.util.internal.CollectionsExt;
+ import org.apache.sis.util.privy.CollectionsExt;
  import org.apache.sis.pending.jdk.JDK19;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 +// Specific to the main branch:
 +import org.apache.sis.metadata.simple.SimpleAttributeType;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/SpecializedIdentifier.java
index 677fe13679,b1f6810ba2..ca2e078830
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/SpecializedIdentifier.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/SpecializedIdentifier.java
@@@ -28,11 -28,11 +28,11 @@@ import org.apache.sis.xml.IdentifierMap
  import org.apache.sis.xml.IdentifierSpace;
  import org.apache.sis.xml.ValueConverter;
  import org.apache.sis.util.resources.Messages;
- import org.apache.sis.util.internal.CloneAccess;
+ import org.apache.sis.util.privy.CloneAccess;
  import org.apache.sis.metadata.iso.citation.Citations;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/cat/CodeListUID.java
index fc68d434bb,e99dd9c49c..d75499ff48
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/cat/CodeListUID.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/cat/CodeListUID.java
@@@ -24,11 -24,11 +24,11 @@@ import jakarta.xml.bind.annotation.XmlV
  import org.opengis.util.CodeList;
  import org.apache.sis.util.iso.Types;
  import org.apache.sis.xml.bind.Context;
- import static org.apache.sis.metadata.internal.ImplementationHelper.ISO_NAMESPACE;
+ import static org.apache.sis.metadata.privy.ImplementationHelper.ISO_NAMESPACE;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.ControlledVocabulary;
 -import org.opengis.annotation.ResourceBundles;
 +// Specific to the main branch:
 +import java.util.ResourceBundle;
- import org.apache.sis.util.internal.CodeLists;
++import org.apache.sis.util.privy.CodeLists;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java
index 9526a3e513,51233a96ab..a629d31582
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TM_Primitive.java
@@@ -19,15 -19,15 +19,15 @@@ package org.apache.sis.xml.bind.gml
  import java.util.Date;
  import jakarta.xml.bind.annotation.XmlElement;
  import org.opengis.temporal.TemporalPrimitive;
- import org.apache.sis.xml.util.XmlUtilities;
+ import org.apache.sis.xml.privy.XmlUtilities;
  import org.apache.sis.xml.bind.Context;
  import org.apache.sis.xml.bind.gco.PropertyType;
- import org.apache.sis.metadata.internal.TemporalUtilities;
+ import org.apache.sis.metadata.privy.TemporalUtilities;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.temporal.Period;
 -import org.opengis.temporal.Instant;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.temporal.Period;
 +import org.apache.sis.pending.geoapi.temporal.Instant;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimeInstant.java
index 92bfd07cc7,d643f739aa..764b47d09b
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimeInstant.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimeInstant.java
@@@ -23,11 -23,11 +23,11 @@@ import jakarta.xml.bind.annotation.XmlT
  import jakarta.xml.bind.annotation.XmlElement;
  import jakarta.xml.bind.annotation.XmlRootElement;
  import org.apache.sis.xml.bind.Context;
- import org.apache.sis.util.internal.Strings;
- import org.apache.sis.xml.util.XmlUtilities;
+ import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.xml.privy.XmlUtilities;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.temporal.Instant;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.temporal.Instant;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriod.java
index 824c683909,e1ce2c79d1..317af7172e
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriod.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriod.java
@@@ -21,11 -21,11 +21,11 @@@ import jakarta.xml.bind.annotation.XmlE
  import jakarta.xml.bind.annotation.XmlElements;
  import jakarta.xml.bind.annotation.XmlRootElement;
  import org.apache.sis.xml.bind.Context;
- import org.apache.sis.util.internal.Strings;
- import static org.apache.sis.xml.util.LegacyNamespaces.VERSION_3_0;
+ import org.apache.sis.util.privy.Strings;
+ import static org.apache.sis.xml.privy.LegacyNamespaces.VERSION_3_0;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.temporal.Period;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.temporal.Period;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gts/TM_PeriodDuration.java
index 68ed3c7776,6024d50a5f..a2d7efbdd7
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gts/TM_PeriodDuration.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gts/TM_PeriodDuration.java
@@@ -25,13 -25,13 +25,13 @@@ import jakarta.xml.bind.annotation.XmlE
  import org.opengis.temporal.PeriodDuration;
  import org.opengis.util.InternationalString;
  import org.apache.sis.xml.bind.Context;
- import org.apache.sis.xml.util.XmlUtilities;
+ import org.apache.sis.xml.privy.XmlUtilities;
  import org.apache.sis.xml.bind.gco.PropertyType;
- import org.apache.sis.metadata.internal.TemporalUtilities;
+ import org.apache.sis.metadata.privy.TemporalUtilities;
  import org.apache.sis.util.SimpleInternationalString;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.temporal.TemporalFactory;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.temporal.TemporalFactory;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/Parameter.java
index d780f5367f,bc9760a5eb..c8f349f4df
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/Parameter.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/Parameter.java
@@@ -22,12 -22,11 +22,12 @@@ import org.opengis.metadata.Identifier
  import org.opengis.parameter.ParameterValue;
  import org.opengis.parameter.ParameterDescriptor;
  import org.apache.sis.metadata.simple.SimpleIdentifiedObject;
- import org.apache.sis.metadata.internal.ReferencingServices;
+ import org.apache.sis.metadata.privy.ReferencingServices;
  import org.apache.sis.util.ComparisonMode;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import static org.apache.sis.util.Utilities.deepEquals;
 +// Specific to the main branch:
 +import java.util.Set;
 +import javax.measure.Unit;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/QualityParameter.java
index 9aab6b4b1c,b05975e830..feb608a93c
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/QualityParameter.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/QualityParameter.java
@@@ -36,8 -36,10 +36,8 @@@ import org.apache.sis.util.iso.Names
  // Specific to the main and geoapi-3.1 branches:
  import org.opengis.referencing.ReferenceIdentifier;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.Coverage;
 -import org.opengis.metadata.quality.Description;
 -import org.opengis.metadata.quality.ValueStructure;
 +// Specific to the main branch:
- import org.apache.sis.metadata.internal.ReferencingServices;
++import org.apache.sis.metadata.privy.ReferencingServices;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/ServiceParameter.java
index a35ed88b98,a6b2c50ba1..f5365cd727
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/ServiceParameter.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/metadata/replace/ServiceParameter.java
@@@ -37,8 -37,9 +37,8 @@@ import static org.apache.sis.util.privy
  // Specific to the main and geoapi-3.1 branches:
  import org.opengis.referencing.ReferenceIdentifier;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.parameter.ParameterDirection;
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
- import org.apache.sis.metadata.internal.ReferencingServices;
++import org.apache.sis.metadata.privy.ReferencingServices;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/privy/IdentifiersTest.java
index 0000000000,0a2c00c26b..2fe41a3f3a
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/privy/IdentifiersTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/privy/IdentifiersTest.java
@@@ -1,0 -1,111 +1,121 @@@
+ /*
+  * 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.metadata.privy;
+ 
+ import java.util.List;
+ import java.util.Arrays;
+ import java.util.ArrayList;
+ import org.opengis.metadata.Identifier;
+ import org.apache.sis.metadata.iso.DefaultIdentifier;
+ import org.apache.sis.metadata.iso.citation.DefaultCitation;
+ 
+ // Test dependencies
+ import org.junit.jupiter.api.Test;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ 
++// Specific to the main branch:
++import org.opengis.referencing.ReferenceIdentifier;
++
+ 
+ /**
+  * Tests {@link Identifiers}.
+  *
+  * @author Martin Desruisseaux (Geomatys)
+  * @versio 1.0
+  */
+ public final class IdentifiersTest extends TestCase {
+     /**
+      * Creates a new test case.
+      */
+     public IdentifiersTest() {
+     }
+ 
+     /**
+      * Creates a citation with the given title and the given identifiers.
+      */
+     private static DefaultCitation citation(final String title, final Identifier... identifiers) {
+         DefaultCitation cit =  new DefaultCitation(title);
+         cit.setIdentifiers(Arrays.asList(identifiers));
+         return cit;
+     }
+ 
+     /**
+      * Creates an identifier with a code space.
+      */
 -    private static Identifier identifier(final String codeSpace, final String code) {
 -        return new DefaultIdentifier(codeSpace, code, null);
++    private static ReferenceIdentifier identifier(final String codeSpace, final String code) {
++        return new Id(codeSpace, code);
++    }
++
++    @SuppressWarnings("serial")
++    private static final class Id extends DefaultIdentifier implements ReferenceIdentifier {
++        Id(String codeSpace, String code) {
++            super(codeSpace, code, null);
++        }
+     }
+ 
+     /**
+      * Tests {@link Identifiers#hasCommonIdentifier(Iterable, Iterable)}.
+      */
+     @Test
+     public void testHasCommonIdentifier() {
 -        final List<Identifier> id1 = new ArrayList<>(3);
 -        final List<Identifier> id2 = new ArrayList<>(2);
++        final List<ReferenceIdentifier> id1 = new ArrayList<>(3);
++        final List<ReferenceIdentifier> id2 = new ArrayList<>(2);
+         assertNull(Identifiers.hasCommonIdentifier(id1, id2));
+         /*
+          * Add codes for two Operation Methods which are implemented in Apache SIS by the same class:
+          *
+          *  - EPSG:9804  —  "Mercator (variant A)" (formerly known as "Mercator (1SP)").
+          *  - EPSG:1026  —  "Mercator (Spherical)"
+          *  - GeoTIFF:7  —  "CT_Mercator"
+          */
+         id1.add(identifier("EPSG", "9804"));
+         id1.add(identifier("EPSG", "1026"));
+         id1.add(identifier("GeoTIFF", "7"));
+         assertNull(Identifiers.hasCommonIdentifier(id1, id2));
+         /*
+          * EPSG:9841 is a legacy (now deprecated) code for "Mercator (1SP)".
+          * We could have declared it as a deprecated code in the above list,
+          * but for the sake of this test we do not.
+          */
+         id2.add(identifier("EPSG", "9841"));
+         assertEquals(Boolean.FALSE, Identifiers.hasCommonIdentifier(id1, id2));
+         id2.add(identifier("EPSG", "9804"));
+         assertEquals(Boolean.TRUE, Identifiers.hasCommonIdentifier(id1, id2));
+     }
+ 
+     /**
+      * Tests {@link Identifiers#getIdentifier(Citation, boolean)}.
+      */
+     @Test
+     public void testGetIdentifier() {
+         DefaultCitation citation = new DefaultCitation(" Not an identifier ");
+         assertEquals("Not an identifier", Identifiers.getIdentifier(citation, false));
+         assertNull(Identifiers.getIdentifier(citation, true));
+ 
+         citation = new DefaultCitation(" ValidIdentifier ");
+         assertEquals("ValidIdentifier", Identifiers.getIdentifier(citation, false));
+         assertEquals("ValidIdentifier", Identifiers.getIdentifier(citation, true));
+         /*
+          * Following test uses '-' in the first identifier, which is an invalid Unicode identifier part.
+          * Consequently, the identifier that we get depends on whether we ask for strict Unicode or not.
+          */
+         citation = citation("Web Map Server", identifier("OGC", "06-042"), identifier("ISO", "19128"));
+         assertEquals("OGC:06-042", Identifiers.getIdentifier(citation, false));
+         assertEquals("ISO_19128",  Identifiers.getIdentifier(citation, true));
+     }
+ }
diff --cc endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/privy/MergerTest.java
index 0000000000,9320f39740..80c3a43e5f
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/privy/MergerTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/privy/MergerTest.java
@@@ -1,0 -1,134 +1,131 @@@
+ /*
+  * 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.metadata.privy;
+ 
+ import java.util.Map;
+ import java.util.Set;
+ import java.util.List;
+ import java.util.Locale;
+ import java.util.Iterator;
+ import java.nio.charset.StandardCharsets;
+ import org.opengis.metadata.citation.Citation;
+ import org.opengis.metadata.content.ContentInformation;
+ import org.opengis.metadata.content.FeatureCatalogueDescription;
+ import org.opengis.metadata.content.ImagingCondition;
+ import org.opengis.metadata.content.ImageDescription;
+ import org.apache.sis.metadata.iso.DefaultIdentifier;
+ import org.apache.sis.metadata.iso.DefaultMetadata;
+ import org.apache.sis.metadata.iso.citation.DefaultCitation;
+ import org.apache.sis.metadata.iso.content.DefaultCoverageDescription;
+ import org.apache.sis.metadata.iso.content.DefaultFeatureCatalogueDescription;
+ import org.apache.sis.metadata.iso.content.DefaultImageDescription;
+ 
+ // Test dependencies
+ import org.junit.jupiter.api.Test;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ import static org.apache.sis.test.Assertions.assertSetEquals;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.content.CoverageDescription;
 -
+ 
+ /**
+  * Tests the {@link Merger} class.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class MergerTest extends TestCase {
+     /**
+      * Creates a new test case.
+      */
+     public MergerTest() {
+     }
+ 
+     /**
+      * Creates a metadata sample with 3 content information of different kind.
+      */
+     private static DefaultMetadata createSample1() {
+         final DefaultFeatureCatalogueDescription features = new DefaultFeatureCatalogueDescription();
+         final DefaultCoverageDescription         coverage = new DefaultCoverageDescription();
+         final DefaultImageDescription            image    = new DefaultImageDescription();
+         final DefaultMetadata                    metadata = new DefaultMetadata();
+         features.setFeatureCatalogueCitations(Set.of(new DefaultCitation("Shapefile")));
+         features.setIncludedWithDataset(Boolean.TRUE);
+         metadata.getContentInfo().add(features);
+ 
+         coverage.setProcessingLevelCode(new DefaultIdentifier("Level 1"));
+         metadata.getContentInfo().add(coverage);
+ 
+         image.setImagingCondition(ImagingCondition.CLOUD);
+         image.setCloudCoverPercentage(0.8);
+         metadata.getContentInfo().add(image);
+ 
+         metadata.setLocalesAndCharsets(Map.of(Locale.JAPANESE, StandardCharsets.UTF_16));
+         return metadata;
+     }
+ 
+     /**
+      * Creates a metadata sample with content information of different kind in a different order
+      * than the one created by {@link #createSample1()}.
+      */
+     private static DefaultMetadata createSample2() {
+         final DefaultFeatureCatalogueDescription features = new DefaultFeatureCatalogueDescription();
+         final DefaultImageDescription            image    = new DefaultImageDescription();
+         final DefaultMetadata                    metadata = new DefaultMetadata();
+         image.setProcessingLevelCode(new DefaultIdentifier("Level 2"));
+         metadata.getContentInfo().add(image);
+ 
+         features.setFeatureCatalogueCitations(Set.of(new DefaultCitation("GPX file")));
+         features.setIncludedWithDataset(Boolean.TRUE);
+         metadata.getContentInfo().add(features);
+ 
+         metadata.setLocalesAndCharsets(Map.of(Locale.FRENCH, StandardCharsets.UTF_8));
+         return metadata;
+     }
+ 
+     /**
+      * Tests a merge operation that merge also the collection elements. Such deep merge is a
+      * little bit aggressive; it may be desired in some occasions, but may also be dangerous.
+      */
+     @Test
+     public void testDeepMerge() {
+         final DefaultMetadata source = createSample1();
+         final DefaultMetadata target = createSample2();
+         final Merger merger = new Merger(null);
+         merger.copy(source, target);
+ 
+         assertSetEquals(List.of(Locale.JAPANESE, Locale.FRENCH),
+                         target.getLocalesAndCharsets().keySet());
+         assertSetEquals(List.of(StandardCharsets.UTF_16, StandardCharsets.UTF_8),
+                         target.getLocalesAndCharsets().values());
+ 
+         final Iterator<ContentInformation> it       = target.getContentInfo().iterator();
+         final ImageDescription             image    = (ImageDescription)            it.next();
+         final FeatureCatalogueDescription  features = (FeatureCatalogueDescription) it.next();
 -        final CoverageDescription          coverage = (CoverageDescription)         it.next();
++        final DefaultCoverageDescription   coverage = (DefaultCoverageDescription)  it.next();
+         assertFalse(it.hasNext());
+ 
+         assertEquals(ImagingCondition.CLOUD, image   .getImagingCondition());
+         assertEquals(Double.valueOf(0.8),    image   .getCloudCoverPercentage());
+         assertEquals("Level 2",              image   .getProcessingLevelCode().getCode());
+         assertEquals("Level 1",              coverage.getProcessingLevelCode().getCode());
+         assertEquals(Boolean.TRUE,           features.isIncludedWithDataset());
+ 
+         final Iterator<? extends Citation> ci = features.getFeatureCatalogueCitations().iterator();
+         assertEquals("GPX file",  ci.next().getTitle().toString());
+         assertEquals("Shapefile", ci.next().getTitle().toString());
+         assertFalse(ci.hasNext());
+     }
+ }
diff --cc endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/style/se1/StyleFactory.java
index c500734661,90e2f83f46..40b2e80ede
--- a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/style/se1/StyleFactory.java
+++ b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/style/se1/StyleFactory.java
@@@ -19,12 -19,13 +19,12 @@@ package org.apache.sis.style.se1
  import java.net.URI;
  import java.awt.Color;
  import java.util.Objects;
- import org.apache.sis.feature.internal.AttributeConvention;
+ import org.apache.sis.feature.privy.AttributeConvention;
  import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.filter.DefaultFilterFactory;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/ModifiableLocationTypeAdapter.java
index e3a7f657fb,0000000000..b2652152c0
mode 100644,000000..100644
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/ModifiableLocationTypeAdapter.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/ModifiableLocationTypeAdapter.java
@@@ -1,101 -1,0 +1,101 @@@
 +/*
 + * 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.referencing.gazetteer;
 +
 +import java.util.List;
 +import java.util.Map;
 +import java.util.IdentityHashMap;
 +import org.opengis.util.InternationalString;
- import org.apache.sis.util.internal.UnmodifiableArrayList;
++import org.apache.sis.util.privy.UnmodifiableArrayList;
 +
 +
 +/**
 + * Workaround for the lack of {@code LocationType} interface in GeoAPI 3.0.
 + * This workaround will be removed if a future GeoAPI version publish that interface,
 + * or if {@link AbstractLocationType} is made public.
 + */
 +final class ModifiableLocationTypeAdapter extends ModifiableLocationType {
 +    /**
 +     * The reference system of the original type.
 +     * This is the only information not stored in {@link ModifiableLocationType}.
 +     */
 +    private final ReferencingByIdentifiers referenceSystem;
 +
 +    /**
 +     * Copies all information from the given type.
 +     */
 +    private ModifiableLocationTypeAdapter(final AbstractLocationType type,
 +            final Map<AbstractLocationType,ModifiableLocationTypeAdapter> previous)
 +    {
 +        super(type.getName());
 +        setTheme(type.getTheme());
 +        setDefinition(type.getDefinition());
 +        setTerritoryOfUse(type.getTerritoryOfUse());
 +        setOwner(type.getOwner());
 +        for (final InternationalString s : type.getIdentifications()) {
 +            addIdentification(s);
 +        }
 +        referenceSystem = type.getReferenceSystem();
 +        for (final AbstractLocationType c : type.getChildren()) {
 +            ModifiableLocationTypeAdapter p = previous.get(c);
 +            if (p == null) {
 +                p = new ModifiableLocationTypeAdapter(c, previous);
 +                previous.put(c, p);
 +            }
 +            p.addParent(this);
 +        }
 +    }
 +
 +    /**
 +     * Returns type type as-is if it is already an instance of {@code ModifiableLocationType},
 +     * or returns a copy otherwise.
 +     */
 +    static ModifiableLocationType copy(final AbstractLocationType type) {
 +        if (type instanceof ModifiableLocationType) {
 +            return (ModifiableLocationType) type;
 +        } else {
 +            return new ModifiableLocationTypeAdapter(type,
 +                    new IdentityHashMap<AbstractLocationType,ModifiableLocationTypeAdapter>());
 +        }
 +    }
 +
 +    /**
 +     * Copies a list of location types.
 +     */
 +    static List<ModifiableLocationType> copy(final List<? extends AbstractLocationType> types) {
 +        final Map<AbstractLocationType,ModifiableLocationTypeAdapter> previous = new IdentityHashMap<>();
 +        final ModifiableLocationType[] nt = new ModifiableLocationType[types.size()];
 +        for (int i=0; i<nt.length; i++) {
 +            final AbstractLocationType c = types.get(i);
 +            ModifiableLocationTypeAdapter p = previous.get(c);
 +            if (p == null) {
 +                p = new ModifiableLocationTypeAdapter(c, previous);
 +                previous.put(c, p);
 +            }
 +            nt[i] = p;
 +        }
 +        return UnmodifiableArrayList.wrap(nt);
 +    }
 +
 +    /**
 +     * Returns the reference system of the type given to the constructor.
 +     */
 +    @Override
 +    public ReferencingByIdentifiers getReferenceSystem() {
 +        return referenceSystem;
 +    }
 +}
diff --cc endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
index 7b10d323c7,c7dd878f07..7d35fb2f14
--- a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
+++ b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
@@@ -40,13 -40,16 +40,13 @@@ import org.apache.sis.io.wkt.ElementKin
  import org.apache.sis.io.wkt.FormattableObject;
  import org.apache.sis.metadata.iso.extent.Extents;
  import org.apache.sis.metadata.iso.citation.Citations;
- import org.apache.sis.referencing.util.WKTUtilities;
+ import org.apache.sis.referencing.privy.WKTUtilities;
  import org.apache.sis.system.Modules;
- import org.apache.sis.util.internal.Constants;
+ import org.apache.sis.util.privy.Constants;
  import org.apache.sis.util.resources.Vocabulary;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.citation.Party;
 -import org.opengis.referencing.gazetteer.Location;
 -import org.opengis.referencing.gazetteer.LocationType;
 -import org.opengis.referencing.gazetteer.ReferenceSystemUsingIdentifiers;
 +// Specific to the main branch:
 +import org.apache.sis.metadata.iso.citation.AbstractParty;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralEnvelope.java
index 0b9d7c46de,ac6e5d7a85..13be0b558b
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralEnvelope.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralEnvelope.java
@@@ -35,9 -35,10 +35,9 @@@ import org.opengis.referencing.crs.Coor
  import org.opengis.geometry.Envelope;
  import org.opengis.geometry.DirectPosition;
  import org.opengis.geometry.MismatchedDimensionException;
 -import org.opengis.geometry.MismatchedReferenceSystemException;
  import org.opengis.metadata.extent.GeographicBoundingBox;
- import org.apache.sis.referencing.util.TemporalAccessor;
- import org.apache.sis.referencing.util.AxisDirections;
+ import org.apache.sis.referencing.privy.TemporalAccessor;
+ import org.apache.sis.referencing.privy.AxisDirections;
  import org.apache.sis.util.ArgumentCheckByAssertion;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.referencing.CRS;
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
index 159812dccf,71eeef9f3d..4e2dccc237
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
@@@ -55,13 -55,13 +55,13 @@@ import org.apache.sis.util.OptionalCand
  import org.apache.sis.util.logging.Logging;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.system.Loggers;
- import org.apache.sis.util.internal.Constants;
- import org.apache.sis.util.internal.StandardDateFormat;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.privy.StandardDateFormat;
  import org.apache.sis.referencing.ImmutableIdentifier;
- import org.apache.sis.referencing.util.ReferencingFactoryContainer;
+ import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.apache.sis.metadata.iso.DefaultIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/ParameterTableRow.java
index 4438dae6b8,583077a13e..d4d929128d
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/ParameterTableRow.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/ParameterTableRow.java
@@@ -40,15 -40,12 +40,15 @@@ import org.apache.sis.util.Characters
  import org.apache.sis.util.Deprecable;
  import org.apache.sis.measure.Range;
  import org.apache.sis.measure.RangeFormat;
- import org.apache.sis.metadata.internal.NameToIdentifier;
- import org.apache.sis.util.internal.X364;
- import static org.apache.sis.util.internal.X364.*;
+ import org.apache.sis.metadata.privy.NameToIdentifier;
+ import org.apache.sis.util.privy.X364;
+ import static org.apache.sis.util.privy.X364.*;
  import static org.apache.sis.util.CharSequences.spaces;
- import static org.apache.sis.util.internal.Constants.DEFAULT_SEPARATOR;
+ import static org.apache.sis.util.privy.Constants.DEFAULT_SEPARATOR;
  
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
 +
  
  /**
   * A row in the table to be formatted by {@link ParameterFormat}.
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
index 329b57d998,d6dfbb4462..d7a3cbd102
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeographicCRS.java
@@@ -33,14 -33,14 +33,14 @@@ import org.apache.sis.referencing.cs.Ab
  import org.apache.sis.io.wkt.Formatter;
  import org.apache.sis.measure.Longitude;
  import org.apache.sis.util.resources.Errors;
- import static org.apache.sis.util.internal.Constants.CRS;
- import static org.apache.sis.util.internal.Constants.EPSG;
- import static org.apache.sis.util.internal.Constants.CRS27;
- import static org.apache.sis.util.internal.Constants.CRS83;
- import static org.apache.sis.util.internal.Constants.CRS84;
+ import static org.apache.sis.util.privy.Constants.CRS;
+ import static org.apache.sis.util.privy.Constants.EPSG;
+ import static org.apache.sis.util.privy.Constants.CRS27;
+ import static org.apache.sis.util.privy.Constants.CRS83;
+ import static org.apache.sis.util.privy.Constants.CRS84;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultEngineeringDatum.java
index df30b214ed,9114ae99ff..10ea0de84d
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultEngineeringDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultEngineeringDatum.java
@@@ -22,11 -22,11 +22,11 @@@ import jakarta.xml.bind.annotation.XmlR
  import org.opengis.util.GenericName;
  import org.opengis.util.InternationalString;
  import org.opengis.referencing.datum.EngineeringDatum;
- import org.apache.sis.referencing.util.WKTKeywords;
+ import org.apache.sis.referencing.privy.WKTKeywords;
  import org.apache.sis.io.wkt.Formatter;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
index 72f895f4fe,c586bc1316..14d96f2598
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java
@@@ -53,10 -53,10 +53,10 @@@ import org.apache.sis.io.wkt.Formatter
  import static org.apache.sis.util.Utilities.deepEquals;
  import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  import static org.apache.sis.util.ArgumentChecks.ensureNonNullElement;
- import static org.apache.sis.referencing.util.WKTUtilities.toFormattable;
+ import static org.apache.sis.referencing.privy.WKTUtilities.toFormattable;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultParametricDatum.java
index f6980fc4e8,1d5096e042..312bd0afef
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultParametricDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultParametricDatum.java
@@@ -20,9 -20,12 +20,9 @@@ import java.util.Date
  import java.util.Map;
  import jakarta.xml.bind.annotation.XmlRootElement;
  import jakarta.xml.bind.annotation.XmlType;
- import org.apache.sis.referencing.util.WKTKeywords;
+ import org.apache.sis.referencing.privy.WKTKeywords;
  import org.apache.sis.io.wkt.Formatter;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.datum.ParametricDatum;
 -
  
  /**
   * Defines the origin of a parametric coordinate reference system.
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
index 5a304dc830,f9b65af4de..44ef22bf56
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java
@@@ -28,13 -28,13 +28,13 @@@ import org.opengis.referencing.datum.Ve
  import org.apache.sis.io.wkt.Formatter;
  import org.apache.sis.util.ComparisonMode;
  import org.apache.sis.xml.bind.Context;
- import org.apache.sis.xml.util.LegacyNamespaces;
- import org.apache.sis.referencing.util.WKTKeywords;
+ import org.apache.sis.xml.privy.LegacyNamespaces;
+ import org.apache.sis.referencing.privy.WKTKeywords;
  import org.apache.sis.referencing.internal.VerticalDatumTypes;
- import org.apache.sis.metadata.internal.ImplementationHelper;
+ import org.apache.sis.metadata.privy.ImplementationHelper;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index eea7d36618,49478aaa44..7ff3d2f510
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@@ -118,14 -118,11 +118,14 @@@ import org.apache.sis.measure.NumberRan
  import org.apache.sis.measure.Units;
  import org.apache.sis.pending.jdk.JDK16;
  import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
- import static org.apache.sis.util.internal.StandardDateFormat.UTC;
+ import static org.apache.sis.util.privy.StandardDateFormat.UTC;
  import static org.apache.sis.referencing.internal.ServicesForMetadata.CONNECTION;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.apache.sis.referencing.internal.ServicesForMetadata;
 +import org.apache.sis.referencing.cs.DefaultParametricCS;
 +import org.apache.sis.referencing.datum.DefaultParametricDatum;
 +import org.apache.sis.referencing.factory.GeodeticObjectFactory;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
index ee688c4ccf,cf1b8f5f42..1682f75053
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
@@@ -22,14 -22,9 +22,14 @@@ import org.opengis.referencing.crs.*
  import org.opengis.referencing.datum.*;
  import org.opengis.referencing.operation.*;
  import org.opengis.parameter.ParameterDescriptor;
- import org.apache.sis.referencing.util.WKTKeywords;
+ import org.apache.sis.referencing.privy.WKTKeywords;
  import org.apache.sis.util.CharSequences;
  
 +// Specific to the main branch:
 +import org.apache.sis.referencing.crs.DefaultParametricCRS;
 +import org.apache.sis.referencing.cs.DefaultParametricCS;
 +import org.apache.sis.referencing.datum.DefaultParametricDatum;
 +
  
  /**
   * Information about a specific table. The MS-Access dialect of SQL is assumed;
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Legacy.java
index 9477b5795a,a5abfd686b..73d7c3de26
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Legacy.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Legacy.java
@@@ -27,21 -27,8 +27,21 @@@ import org.apache.sis.referencing.Commo
  import org.apache.sis.referencing.cs.CoordinateSystems;
  import org.apache.sis.referencing.cs.DefaultCartesianCS;
  import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
- import org.apache.sis.referencing.util.ReferencingUtilities;
+ import org.apache.sis.referencing.privy.ReferencingUtilities;
  
 +// Specific to the main branch:
 +import java.util.List;
 +import java.util.Objects;
 +import java.util.Collection;
 +import org.opengis.referencing.ReferenceSystem;
 +import org.opengis.referencing.IdentifiedObject;
 +import org.opengis.util.InternationalString;
 +import org.opengis.metadata.extent.Extent;
 +import org.opengis.referencing.datum.Datum;
 +import org.opengis.referencing.operation.CoordinateOperation;
 +import org.apache.sis.referencing.DefaultObjectDomain;
 +import org.apache.sis.referencing.AbstractIdentifiedObject;
 +
  
  /**
   * Utilities related to version 1 of Well Known Text format, or to ISO 19111:2007.
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ServicesForMetadata.java
index 8aed5ce6bc,4a6635c71b..e4d5b83d55
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ServicesForMetadata.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ServicesForMetadata.java
@@@ -46,7 -46,6 +46,7 @@@ import org.apache.sis.geometry.Envelope
  import org.apache.sis.geometry.AbstractEnvelope;
  import org.apache.sis.geometry.DirectPosition2D;
  import org.apache.sis.geometry.CoordinateFormat;
- import org.apache.sis.metadata.internal.ReferencingServices;
++import org.apache.sis.metadata.privy.ReferencingServices;
  import org.apache.sis.referencing.CRS;
  import org.apache.sis.referencing.CommonCRS;
  import org.apache.sis.referencing.IdentifiedObjects;
@@@ -69,26 -69,8 +69,26 @@@ import org.apache.sis.util.privy.Consta
  import org.apache.sis.util.resources.Vocabulary;
  import org.apache.sis.util.logging.Logging;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import java.util.Map;
 +import org.opengis.util.NoSuchIdentifierException;
 +import org.opengis.util.TypeName;
 +import org.opengis.referencing.ReferenceIdentifier;
 +import org.opengis.referencing.crs.CRSFactory;
 +import org.opengis.referencing.cs.CSFactory;
 +import org.opengis.referencing.cs.CoordinateSystemAxis;
 +import org.opengis.referencing.operation.MathTransformFactory;
 +import org.opengis.referencing.operation.OperationMethod;
 +import org.opengis.referencing.operation.SingleOperation;
 +import org.opengis.referencing.datum.Datum;
 +import org.opengis.referencing.datum.DatumFactory;
- import org.apache.sis.metadata.internal.NameToIdentifier;
++import org.apache.sis.metadata.privy.NameToIdentifier;
 +import org.apache.sis.util.Deprecable;
 +import org.apache.sis.referencing.cs.DefaultParametricCS;
 +import org.apache.sis.referencing.datum.DefaultParametricDatum;
 +import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 +import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 +import org.apache.sis.metadata.iso.citation.DefaultCitation;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
index 48cd4acc77,093ba91f45..118f2a75d7
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
@@@ -49,15 -49,15 +49,15 @@@ import org.apache.sis.referencing.opera
  import org.apache.sis.referencing.operation.transform.MathTransformProvider;
  import org.apache.sis.referencing.operation.transform.DomainDefinition;
  import org.apache.sis.referencing.operation.provider.MapProjection;
- import org.apache.sis.referencing.util.CoordinateOperations;
- import org.apache.sis.referencing.util.Formulas;
+ import org.apache.sis.referencing.privy.CoordinateOperations;
+ import org.apache.sis.referencing.privy.Formulas;
  import org.apache.sis.system.Modules;
- import org.apache.sis.util.internal.Constants;
- import org.apache.sis.util.internal.Numerics;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.privy.Numerics;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
index 0000000000,f660488a99..408a075fb6
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/AxisDirections.java
@@@ -1,0 -1,834 +1,831 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.Objects;
+ import javax.measure.Unit;
+ import javax.measure.quantity.Angle;
+ import org.opengis.annotation.UML;
+ import org.opengis.referencing.cs.AxisDirection;
+ import org.opengis.referencing.cs.CoordinateSystem;
+ import org.opengis.referencing.cs.CoordinateSystemAxis;
+ import static org.opengis.referencing.cs.AxisDirection.*;
+ import static org.opengis.annotation.Obligation.CONDITIONAL;
+ import org.apache.sis.metadata.privy.NameToIdentifier;
+ import org.apache.sis.util.ComparisonMode;
+ import org.apache.sis.util.Characters;
+ import org.apache.sis.util.Utilities;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.Static;
+ import org.apache.sis.util.iso.Types;
+ import org.apache.sis.measure.Units;
+ import static org.apache.sis.util.CharSequences.*;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import static org.opengis.annotation.Specification.ISO_19162;
++// Specific to the main branch:
++import static org.opengis.annotation.Specification.UNSPECIFIED;
+ 
+ 
+ /**
+  * Utilities methods related to {@link AxisDirection}.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class AxisDirections extends Static {
+     /**
+      * Number of directions like "North", "North-North-East", "North-East", etc.
+      * The first of those directions is {@link AxisDirection#NORTH}.
+      */
+     public static final int COMPASS_COUNT = 16;
+ 
+     /**
+      * Number of geocentric directions.
+      * The first of those directions is {@link AxisDirection#GEOCENTRIC_X}.
+      */
+     public static final int GEOCENTRIC_COUNT = 3;
+ 
+     /**
+      * Number of directions like "Display right", "Display down", etc.
+      * The first of those directions is {@link AxisDirection#DISPLAY_RIGHT}.
+      */
+     public static final int DISPLAY_COUNT = 4;
+ 
+     /**
+      * Ordinal of the last element in the {@link AxisDirection} code list.
+      * This is used for differentiating the standard codes from the user-defined ones.
+      *
+      * @see #isUserDefined(AxisDirection)
+      */
+     private static final int LAST_ORDINAL = DISPLAY_DOWN.ordinal();
+ 
+     /**
+      * Forward direction.
+      * For an observer at the centre of the object this is will be towards its front, bow or nose.
+      * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+      */
 -    @UML(identifier="forward", obligation=CONDITIONAL, specification=ISO_19162)
++    @UML(identifier="forward", obligation=CONDITIONAL, specification=UNSPECIFIED)
+     public static final AxisDirection FORWARD = AxisDirection.valueOf("FORWARD");
 -    /*
 -     * TODO: remove @Disabled in `WKTParserTest` after the code list values in this class have been removed.
 -     */
+ 
+     /**
+      * Starboard direction.
+      * For an observer at the centre of the object this will be towards its right.
+      * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+      */
 -    @UML(identifier="starboard", obligation=CONDITIONAL, specification=ISO_19162)
++    @UML(identifier="starboard", obligation=CONDITIONAL, specification=UNSPECIFIED)
+     public static final AxisDirection STARBOARD = AxisDirection.valueOf("STARBOARD");
+ 
+     /**
+      * Port direction.
+      * For an observer at the centre of the object this will be towards its left.
+      * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+      */
 -    @UML(identifier="port", obligation=CONDITIONAL, specification=ISO_19162)
++    @UML(identifier="port", obligation=CONDITIONAL, specification=UNSPECIFIED)
+     public static final AxisDirection PORT = AxisDirection.valueOf("PORT");
+ 
+     /**
+      * Direction of geographic angles (bearing).
+      * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+      */
 -    @UML(identifier="clockwise", obligation=CONDITIONAL, specification=ISO_19162)
++    @UML(identifier="clockwise", obligation=CONDITIONAL, specification=UNSPECIFIED)
+     public static final AxisDirection CLOCKWISE = AxisDirection.valueOf("CLOCKWISE");
+ 
+     /**
+      * Direction of arithmetic angles. Used in polar coordinate systems.
+      * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+      */
 -    @UML(identifier="counterClockwise", obligation=CONDITIONAL, specification=ISO_19162)
++    @UML(identifier="counterClockwise", obligation=CONDITIONAL, specification=UNSPECIFIED)
+     public static final AxisDirection COUNTER_CLOCKWISE = AxisDirection.valueOf("COUNTER_CLOCKWISE");
+ 
+     /**
+      * Distance from the origin in a polar coordinate system.
+      * Added in ISO 19111:2019 (was not in ISO 19111:2007).
+      */
 -    @UML(identifier="awayFrom", obligation=CONDITIONAL, specification=ISO_19162)
++    @UML(identifier="awayFrom", obligation=CONDITIONAL, specification=UNSPECIFIED)
+     public static final AxisDirection AWAY_FROM = AxisDirection.valueOf("AWAY_FROM");
+ 
+     /**
+      * For each direction, the opposite direction.
+      * This map shall be immutable after construction.
+      */
+     private static final Map<AxisDirection,AxisDirection> OPPOSITES = new HashMap<>(20);
+     static {
+         put(OTHER,             OTHER);
+         put(NORTH,             SOUTH);
+         put(NORTH_NORTH_EAST,  SOUTH_SOUTH_WEST);
+         put(NORTH_EAST,        SOUTH_WEST);
+         put(EAST_NORTH_EAST,   WEST_SOUTH_WEST);
+         put(EAST,              WEST);
+         put(EAST_SOUTH_EAST,   WEST_NORTH_WEST);
+         put(SOUTH_EAST,        NORTH_WEST);
+         put(SOUTH_SOUTH_EAST,  NORTH_NORTH_WEST);
+         put(UP,                DOWN);
+         put(FUTURE,            PAST);
+         put(COLUMN_POSITIVE,   COLUMN_NEGATIVE);
+         put(ROW_POSITIVE,      ROW_NEGATIVE);
+         put(DISPLAY_RIGHT,     DISPLAY_LEFT);
+         put(DISPLAY_UP,        DISPLAY_DOWN);
+         put(COUNTER_CLOCKWISE, CLOCKWISE);
+     }
+ 
+     /**
+      * Stores the given directions in the {@link #OPPOSITES} array.
+      */
+     private static void put(final AxisDirection dir, final AxisDirection opposite) {
+         OPPOSITES.put(dir, opposite);
+         OPPOSITES.put(opposite, dir);
+     }
+ 
+     /**
+      * Proposed abbreviations for some axis directions.
+      */
+     private static final Map<AxisDirection,String> ABBREVIATIONS = Map.of(
+             FUTURE,            "t",
+             COLUMN_POSITIVE,   "i",
+             ROW_POSITIVE,      "j",
+             DISPLAY_RIGHT,     "x",
+             DISPLAY_UP,        "y",
+             OTHER,             "z",     // Arbitrary abbreviation, may change in any future SIS version.
+             AWAY_FROM,         "r",
+             COUNTER_CLOCKWISE, "θ");
+ 
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private AxisDirections() {
+     }
+ 
+     /**
+      * Returns the "absolute" direction of the given direction.
+      * This "absolute" operation is similar to the {@code Math.abs(int)} method in that "negative" directions like
+      * ({@code SOUTH}, {@code WEST}, {@code DOWN}, {@code PAST}) are changed for their "positive" counterparts
+      * ({@code NORTH}, {@code EAST}, {@code UP}, {@code FUTURE}).
+      * More specifically, the following conversion table is applied:
+      *
+      * <table class="sis">
+      * <caption>Mapping to "absolute" directions</caption><tr>
+      * <tr><th>Directions</th>                                      <th>Absolute value</th></tr>
+      * <tr><td>{@code NORTH},           {@code SOUTH}</td>            <td>{@code NORTH}</td></tr>
+      * <tr><td>{@code EAST},            {@code WEST}</td>             <td>{@code EAST}</td></tr>
+      * <tr><td>{@code UP},              {@code DOWN}</td>             <td>{@code UP}</td></tr>
+      * <tr><td>{@code FUTURE},          {@code PAST}</td>             <td>{@code FUTURE}</td></tr>
+      * <tr><td>{@code COLUMN_POSITIVE}, {@code COLUMN_NEGATIVE}</td>  <td>{@code COLUMN_POSITIVE}</td></tr>
+      * <tr><td>{@code ROW_POSITIVE},    {@code ROW_NEGATIVE}</td>     <td>{@code ROW_POSITIVE}</td></tr>
+      * <tr><td>{@code DISPLAY_RIGHT},   {@code DISPLAY_LEFT}</td>     <td>{@code DISPLAY_RIGHT}</td></tr>
+      * <tr><td>{@code DISPLAY_UP},      {@code DISPLAY_DOWN}</td>     <td>{@code DISPLAY_UP}</td></tr>
+      * <tr><td>{@code CLOCKWISE},       {@code COUNTERCLOCKWISE}</td> <td>{@code COUNTERCLOCKWISE}</td></tr>
+      * <tr><td>{@code OTHER}</td>                                     <td>{@code OTHER}</td></tr>
+      * </table>
+      *
+      * @param  dir  the direction for which to return the absolute direction, or {@code null}.
+      * @return the direction from the above table, or {@code null} if the given direction was null.
+      */
+     public static AxisDirection absolute(AxisDirection dir) {
+         final AxisDirection opposite = opposite(dir);
+         if (opposite != null) {
+             if (opposite.ordinal() < dir.ordinal()) {
+                 dir = opposite;
+             }
+             // Below is a temporary patch pending integration of code list values into GeoAPI.
+             // We need this patch because we cannot rely on ordinal() value for custom codes.
+             if (dir == CLOCKWISE) {
+                 dir = COUNTER_CLOCKWISE;
+             }
+         }
+         return dir;
+     }
+ 
+     /**
+      * Returns the opposite direction of the given direction. The opposite direction of
+      * {@code NORTH} is {@code SOUTH}, and the opposite direction of {@code SOUTH} is {@code NORTH}.
+      * The same applies to {@code EAST}-{@code WEST}, {@code UP}-{@code DOWN} and {@code FUTURE}-{@code PAST},
+      * <i>etc.</i> If the given axis direction has no opposite, then this method returns {@code null}.
+      *
+      * @param  dir  the direction for which to return the opposite direction, or {@code null}.
+      * @return the opposite direction, or {@code null} if none or unknown.
+      */
+     public static AxisDirection opposite(AxisDirection dir) {
+         return OPPOSITES.get(dir);
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is an "opposite" direction.
+      * If the given argument is {@code null} or is not a known direction, then
+      * this method conservatively returns {@code false}.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is an "opposite".
+      */
+     public static boolean isOpposite(final AxisDirection dir) {
+         final AxisDirection opposite = opposite(dir);
+         return (opposite != null) && opposite.ordinal() < dir.ordinal();
+     }
+ 
+     /**
+      * Returns {@code true} if the specified direction is a compass direction.
+      * Compass directions are {@code NORTH}, {@code EAST}, {@code NORTH_EAST}, etc.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is a compass direction.
+      *
+      * @see #angleForCompass(AxisDirection, AxisDirection)
+      */
+     public static boolean isCompass(final AxisDirection dir) {
+         if (dir == null) return false;
+         final int n  = dir.ordinal() - NORTH.ordinal();
+         return n >= 0 && n < COMPASS_COUNT;
+     }
+ 
+     /**
+      * Returns {@code true} if the specified direction is cardinal direction.
+      * Cardinal directions are {@code NORTH}, {@code SOUTH}, {@code EAST} and {@code WEST}.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is a cardinal direction.
+      */
+     public static boolean isCardinal(final AxisDirection dir) {
+         if (dir == null) return false;
+         final int n  = dir.ordinal() - NORTH.ordinal();
+         return n >= 0 && n < COMPASS_COUNT && (n & 3) == 0;
+     }
+ 
+     /**
+      * Returns {@code true} if the specified direction is an inter-cardinal direction.
+      * Inter-cardinal directions are {@code NORTH_EAST}, {@code SOUTH_SOUTH_EAST}, etc.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is an inter-cardinal direction.
+      */
+     public static boolean isIntercardinal(final AxisDirection dir) {
+         if (dir == null) return false;
+         final int n  = dir.ordinal() - NORTH.ordinal();
+         return n >= 0 && n < COMPASS_COUNT && (n & 3) != 0;
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is {@code UP} or {@code DOWN}.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the direction is vertical, or {@code false} otherwise.
+      */
+     public static boolean isVertical(final AxisDirection dir) {
+         return dir == UP || dir == DOWN;
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is {@code FUTURE} or {@code PAST}.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the direction is temporal, or {@code false} otherwise.
+      */
+     public static boolean isTemporal(final AxisDirection dir) {
+         return dir == FUTURE || dir == PAST;
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is {@code GEOCENTRIC_X}, {@code GEOCENTRIC_Y}
+      * or {@code GEOCENTRIC_Z}.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is one of geocentric directions.
+      */
+     public static boolean isGeocentric(final AxisDirection dir) {
+         return dir == GEOCENTRIC_X || dir == GEOCENTRIC_Y || dir == GEOCENTRIC_Z;
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is a spatial axis direction (including vertical and geocentric axes).
+      * The current implementation conservatively returns {@code true} for every non-null directions except a hard-coded
+      * set of directions which are known to be non-spatial. We conservatively accept unknown axis directions because
+      * some of them are created from strings like "South along 90°E".
+      *
+      * <p>If the {@code image} argument is {@code true}, then this method additionally accepts grid and display
+      * axis directions.</p>
+      *
+      * <p>The rules implemented by this method may change in any future SIS version.</p>
+      *
+      * @param  dir    the direction to test, or {@code null}.
+      * @param  image  {@code true} for accepting grid and image axis directions in addition to spatial ones.
+      * @return {@code true} if the given direction is presumed for spatial CS.
+      */
+     public static boolean isSpatialOrUserDefined(final AxisDirection dir, final boolean image) {
+         if (dir == null) return false;
+         final int ordinal = dir.ordinal();
+         return ordinal < FUTURE.ordinal() || ordinal > (image ? PAST : DISPLAY_DOWN).ordinal();
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is a user-defined direction (i.e. is not defined by GeoAPI).
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is user-defined.
+      */
+     public static boolean isUserDefined(final AxisDirection dir) {
+         return (dir != null) && dir.ordinal() > LAST_ORDINAL;
+     }
+ 
+     /**
+      * Returns {@code true} if the given direction is {@code COLUMN_POSITIVE}, {@code COLUMN_NEGATIVE},
+      * {@code ROW_POSITIVE} or {@code ROW_NEGATIVE}.
+      *
+      * @param  dir  the direction to test, or {@code null}.
+      * @return {@code true} if the given direction is presumed for grid CS.
+      */
+     public static boolean isGrid(final AxisDirection dir) {
+         if (dir == null) return false;
+         final int ordinal = dir.ordinal();
+         return ordinal >= COLUMN_POSITIVE.ordinal() && ordinal <= ROW_NEGATIVE.ordinal();
+     }
+ 
+     /**
+      * Arithmetic angle between forward/aft/port/starboard directions only.
+      * This is the angle as viewed from above the vehicle.
+      *
+      * @param  source  the start direction.
+      * @param  target  the final direction.
+      * @return the angle as a multiple of 90°, or {@link Integer#MIN_VALUE} if none.
+      */
+     public static int angleForVehicle(final AxisDirection source, final AxisDirection target) {
+         if (source == STARBOARD && target == FORWARD) return +1;
+         if (source == FORWARD && target == STARBOARD) return -1;
+         return Integer.MIN_VALUE;
+     }
+ 
+     /**
+      * Angle between geocentric directions only.
+      *
+      * @param  source  the start direction.
+      * @param  target  the final direction.
+      * @return the angle as a multiple of 90°, or {@link Integer#MIN_VALUE} if none.
+      */
+     public static int angleForGeocentric(final AxisDirection source, final AxisDirection target) {
+         final int base = GEOCENTRIC_X.ordinal();
+         final int src  = source.ordinal() - base;
+         if (src >= 0 && src < GEOCENTRIC_COUNT) {
+             final int tgt = target.ordinal() - base;
+             if (tgt >= 0 && tgt < GEOCENTRIC_COUNT) {
+                 int n = (tgt - src);
+                 n -= GEOCENTRIC_COUNT * (n/2);      // If -2 add 3.  If +2 subtract 3.  Otherwise do nothing.
+                 return n;
+             }
+         }
+         return Integer.MIN_VALUE;
+     }
+ 
+     /**
+      * Arithmetic angle between compass directions only (not for angle between direction along meridians).
+      *
+      * @param  source  the start direction.
+      * @param  target  the final direction.
+      * @return the arithmetic angle as a multiple of 360/{@link #COMPASS_COUNT}, or {@link Integer#MIN_VALUE} if none.
+      *
+      * @see #isCompass(AxisDirection)
+      */
+     public static int angleForCompass(final AxisDirection source, final AxisDirection target) {
+         final int base = NORTH.ordinal();
+         final int src  = source.ordinal() - base;
+         if (src >= 0 && src < COMPASS_COUNT) {
+             final int tgt = target.ordinal() - base;
+             if (tgt >= 0 && tgt < COMPASS_COUNT) {
+                 int n = src - tgt;
+                 if (n < -COMPASS_COUNT/2) {
+                     n += COMPASS_COUNT;
+                 } else if (n > COMPASS_COUNT/2) {
+                     n -= COMPASS_COUNT;
+                 }
+                 return n;
+             }
+         }
+         return Integer.MIN_VALUE;
+     }
+ 
+     /**
+      * Arithmetic angle between display directions only.
+      *
+      * @param  source  the start direction.
+      * @param  target  the final direction.
+      * @return the arithmetic angle as a multiple of 360/{@link #DISPLAY_COUNT}, or {@link Integer#MIN_VALUE} if none.
+      */
+     public static int angleForDisplay(final AxisDirection source, final AxisDirection target) {
+         final int base = DISPLAY_RIGHT.ordinal();
+         int src  = source.ordinal() - base;
+         if (src >= 0 && src < DISPLAY_COUNT) {
+             int tgt = target.ordinal() - base;
+             if (tgt >= 0 && tgt < DISPLAY_COUNT) {
+                 /*
+                  * Display directions are RIGHT, LEFT, UP, DOWN. We need to reorder them as UP, RIGHT, DOWN, LEFT.
+                  */
+                 src = DISPLAY_ORDER[src];
+                 tgt = DISPLAY_ORDER[tgt];
+                 int n = src - tgt;
+                 if (n < -DISPLAY_COUNT/2) {
+                     n += DISPLAY_COUNT;
+                 } else if (n > DISPLAY_COUNT/2) {
+                     n -= DISPLAY_COUNT;
+                 }
+                 return n;
+             }
+         }
+         return Integer.MIN_VALUE;
+     }
+ 
+     /**
+      * Maps RIGHT, LEFT, UP, DOWN display order to UP, RIGHT, DOWN, LEFT.
+      */
+     private static final byte[] DISPLAY_ORDER = {1, 3, 0, 2};
+ 
+     /**
+      * Returns the angular unit of the specified coordinate system.
+      * The preference will be given to the longitude axis, if found.
+      *
+      * @param  cs        the coordinate system from which to get the angular unit, or {@code null}.
+      * @param  fallback  the default unit to return if no angular unit is found.
+      * @return the angular unit, of {@code unit} if no angular unit was found.
+      *
+      * @see org.apache.sis.referencing.privy.ReferencingUtilities#getUnit(CoordinateSystem)
+      */
+     public static Unit<Angle> getAngularUnit(final CoordinateSystem cs, Unit<Angle> fallback) {
+         if (cs != null) {
+             for (int i = cs.getDimension(); --i>=0;) {
+                 final CoordinateSystemAxis axis = cs.getAxis(i);
+                 if (axis != null) {                                                     // Paranoiac check.
+                     final Unit<?> candidate = axis.getUnit();
+                     if (Units.isAngular(candidate)) {
+                         fallback = candidate.asType(Angle.class);
+                         if (absolute(axis.getDirection()) == AxisDirection.EAST) {
+                             break;                                                      // Found the longitude axis.
+                         }
+                     }
+                 }
+             }
+         }
+         return fallback;
+     }
+ 
+     /**
+      * Finds the dimension of an axis having the given direction or its opposite.
+      * If more than one axis has the given direction, only the first occurrence is returned.
+      * If both the given direction and its opposite exist, then the dimension for the given
+      * direction has precedence over the opposite direction.
+      *
+      * @param  cs         the coordinate system to inspect, or {@code null}.
+      * @param  direction  the direction of the axis to search.
+      * @return the dimension of the axis using the given direction or its opposite, or -1 if none.
+      */
+     public static int indexOfColinear(final CoordinateSystem cs, final AxisDirection direction) {
+         int fallback = -1;
+         if (cs != null) {
+             final int dimension = cs.getDimension();
+             for (int i=0; i<dimension; i++) {
+                 final AxisDirection d = cs.getAxis(i).getDirection();
+                 if (direction.equals(d)) {
+                     return i;
+                 }
+                 if (fallback < 0 && d.equals(opposite(direction))) {
+                     fallback = i;
+                 }
+             }
+         }
+         return fallback;
+     }
+ 
+     /**
+      * Returns the index of the first dimension in {@code cs} where axes are colinear with the {@code subCS} axes.
+      * If no such dimension is found, returns -1. If more than one sequence of {@code cs} axes are colinear with
+      * all {@code subCS} axes, then the following rules apply:
+      *
+      * <ol>
+      *   <li>If a sequence of {@code cs} axes are equal to the {@code subCS} axes, that sequence has precedence.</li>
+      *   <li>Otherwise if a sequence of {@code cs} axes have similar names than the {@code subCS} axes (as determined
+      *       by {@linkplain org.apache.sis.referencing.IdentifiedObjects#isHeuristicMatchForName heuristic match},
+      *       that sequence has precedence.</li>
+      *   <li>Otherwise the index of the first sequence is returned, regardless axis names.</li>
+      * </ol>
+      *
+      * Note that colinear axes are normally not allowed, except in the case of {@link org.opengis.referencing.crs.TemporalCRS}
+      * when one time axis is the runtime (the date where a numerical model has been executed) and the other time axis is the
+      * forecast time (the date at which a prevision is made).
+      *
+      * @param  cs     the coordinate system which contains all axes, or {@code null}.
+      * @param  subCS  the coordinate system to search into {@code cs}.
+      * @return the first dimension of a sequence of axes colinear with {@code subCS} axes, or {@code -1} if none.
+      */
+     public static int indexOfColinear(final CoordinateSystem cs, final CoordinateSystem subCS) {
+         int fallback = -1;
+         if (cs != null) {
+             boolean fallbackMatches = false;
+             final int subDim = subCS.getDimension();
+             final int limit = cs.getDimension() - subDim;
+ next:       for (int i=0; i <= limit; i++) {
+                 boolean equal = true;
+                 boolean match = true;
+                 for (int j=0; j<subDim; j++) {
+                     final CoordinateSystemAxis expected = subCS.getAxis(j);
+                     final CoordinateSystemAxis actual = cs.getAxis(i + j);
+                     if (!isColinear(expected.getDirection(), actual.getDirection())) {
+                         continue next;
+                     }
+                     if (equal) {
+                         equal = Utilities.deepEquals(expected, actual, ComparisonMode.BY_CONTRACT);
+                         if (equal) continue;
+                     }
+                     if (match) {
+                         match = NameToIdentifier.isHeuristicMatchForName(actual, expected.getName().getCode());
+                     }
+                 }
+                 if (equal) {
+                     return i;
+                 }
+                 if (fallback < 0 | (match & !fallbackMatches)) {
+                     fallbackMatches = match;
+                     fallback = i;
+                 }
+             }
+         }
+         return fallback;
+     }
+ 
+     /**
+      * Returns whether the second axis is colinear with the first axis. This method returns {@code true}
+      * if the {@linkplain #absolute absolute} direction of the given directions are equal.
+      * For example, "down" is considered colinear with "up".
+      *
+      * @param  d1  the first axis direction to compare.
+      * @param  d2  the second axis direction to compare.
+      * @return {@code true} if both directions are colinear.
+      */
+     public static boolean isColinear(final AxisDirection d1, final AxisDirection d2) {
+         return Objects.equals(absolute(d1), absolute(d2));
+     }
+ 
+     /**
+      * Returns the indices of {@code cs} axes presumed covariant with {@code subCS} axes.
+      * The mapping is based on axis directions only, with colinear axes mapped in priority.
+      * If some axes cannot be mapped using collinearity criterion, then directions from poles
+      * (e.g. <q>South along 90°E</q>) are arbitrarily handled as if they were covariant
+      * with East and North directions, in that order.
+      *
+      * @param  cs     the coordinate system which contains all axes, or {@code null}.
+      * @param  subCS  the coordinate system for which to search axes into {@code cs}.
+      * @return indices in {@code cs} of axes covariant with {@code subCS} axes in the order they appear in {@code subCS},
+      *         or {@code null} if at least one {@code subCS} axis cannot be mapped to a {@code cs} axis.
+      *
+      * @see #indexOfColinear(CoordinateSystem, CoordinateSystem)
+      */
+     public static int[] indicesOfLenientMapping(final CoordinateSystem cs, final CoordinateSystem subCS) {
+         final int index = indexOfColinear(cs, subCS);           // More robust than fallback below.
+         if (index >= 0) {
+             return ArraysExt.range(index, index + subCS.getDimension());
+         }
+         return AxesMapper.indices(cs, subCS);
+     }
+ 
+     /**
+      * Searches for an axis direction having the given name in the specified list of directions.
+      * This method compares the given name with the name of each {@code AxisDirection} in a lenient way:
+      *
+      * <ul>
+      *   <li>Comparisons are case-insensitive.</li>
+      *   <li>Any character which is not a letter or a digit is ignored. For example, {@code "NorthEast"},
+      *       {@code "North-East"} and {@code "NORTH_EAST"} are considered equivalent.</li>
+      *   <li>This method accepts abbreviations as well, for example if the given {@code name} is {@code "W"},
+      *       then it will be considered equivalent to {@code "WEST"}.</li>
+      * </ul>
+      *
+      * @param  name        the name of the axis direction to search.
+      * @param  directions  the list of axis directions in which to search.
+      * @return the first axis direction having a name matching the given one, or {@code null} if none.
+      */
+     public static AxisDirection find(final String name, final AxisDirection[] directions) {
+         for (final AxisDirection candidate : directions) {
+             final String identifier = candidate.name();
+             if (equalsFiltered(name, identifier, Characters.Filter.LETTERS_AND_DIGITS, true)
+                     || isAcronymForWords(name, identifier))
+             {
+                 return candidate;
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Searches predefined {@link AxisDirection} for a given name. This method searches for a match in the set
+      * of known axis directions as returned by {@link AxisDirection#values()}, plus a few special cases like
+      * <q>Geocentre &gt; equator/90°E</q>. The latter are used in the EPSG database for geocentric CRS.
+      *
+      * <p>This method does not know about {@code org.apache.sis.referencing.cs.DirectionAlongMeridian}.
+      * The latter is a parser which may create new directions, while this method searches only in a set
+      * of predefined directions and never create new ones.</p>
+      *
+      * @param  name  the name of the axis direction to search.
+      * @return the first axis direction having a name matching the given one, or {@code null} if none.
+      */
+     public static AxisDirection valueOf(String name) {
+         name = name.replace('_', ' ').strip();
+         final AxisDirection[] directions = AxisDirection.values();
+         AxisDirection candidate = find(name, directions);
+         if (candidate == null) {
+             /*
+              * No match found when using the predefined axis name. Searches among
+              * the set of geocentric directions. Expected directions are:
+              *
+              *    Geocentre > equator/PM      or    Geocentre > equator/0°E
+              *    Geocentre > equator/90dE    or    Geocentre > equator/90°E
+              *    Geocentre > north pole
+              */
+             int d = name.indexOf('>');
+             if (d >= 0 && equalsIgnoreCase(name, 0, skipTrailingWhitespaces(name, 0, d), "Geocentre")) {
+                 final int length = name.length();
+                 d = skipLeadingWhitespaces(name, d+1, length);
+                 int s = name.indexOf('/', d);
+                 if (s < 0) {
+                     if (equalsIgnoreCase(name, d, length, "north pole")) {
+                         return GEOCENTRIC_Z;                                // "Geocentre > north pole"
+                     }
+                 } else if (equalsIgnoreCase(name, d, skipTrailingWhitespaces(name, d, s), "equator")) {
+                     s = skipLeadingWhitespaces(name, s+1, length);
+                     if (equalsIgnoreCase(name, s, length, "PM")) {
+                         return GEOCENTRIC_X;                                // "Geocentre > equator/PM"
+                     }
+                     /*
+                      * At this point, the name may be "Geocentre > equator/0°E",
+                      * "Geocentre > equator/90°E" or "Geocentre > equator/90dE".
+                      * Parse the number, limiting the scan to 6 characters for
+                      * avoiding a NumberFormatException.
+                      */
+                     final int stopAt = Math.min(s + 6, length);
+                     for (int i=s; i<stopAt; i++) {
+                         final char c = name.charAt(i);
+                         if (c < '0' || c > '9') {
+                             if (i == s) break;
+                             final int n = Integer.parseInt(name.substring(s, i));
+                             i = skipLeadingWhitespaces(name, i, length);
+                             if (equalsIgnoreCase(name, i, length, "°E") || equalsIgnoreCase(name, i, length, "dE")) {
+                                 switch (n) {
+                                     case  0: return GEOCENTRIC_X;           // "Geocentre > equator/0°E"
+                                     case 90: return GEOCENTRIC_Y;           // "Geocentre > equator/90°E"
+                                 }
+                             }
+                             break;
+                         }
+                     }
+                 }
+             }
+         }
+         return candidate;
+     }
+ 
+     /**
+      * Returns {@code true} if the given sub-sequence is equal to the given keyword, ignoring case.
+      */
+     private static boolean equalsIgnoreCase(final String name, final int lower, final int upper, final String keyword) {
+         final int length = upper - lower;
+         return (length == keyword.length()) && name.regionMatches(true, lower, keyword, 0, length);
+     }
+ 
+     /**
+      * Returns {@code true} if the given name starts or ends with the given keyword, ignoring case.
+      *
+      * @param end  {@code false} if the given keyword is expected at the beginning of the name,
+      *             or {@code true} if expected at the end.
+      */
+     private static boolean contains(final String name, final String keyword, final boolean end) {
+         final int length = keyword.length();
+         return name.regionMatches(true, end ? name.length() - length : 0, keyword, 0, length);
+     }
+ 
+     /**
+      * Suggests an abbreviation for the given axis direction. The unit of measurement may be used
+      * for resolving some ambiguities like whether {@link AxisDirection#EAST} is for "x" (Easting)
+      * or "λ" (Longitude).
+      *
+      * @param  name       the axis name for which to suggest an abbreviation.
+      * @param  direction  the axis direction for which to suggest an abbreviation.
+      * @param  unit       the axis unit of measurement, for disambiguation.
+      * @return a suggested abbreviation.
+      */
+     public static String suggestAbbreviation(final String name, final AxisDirection direction, final Unit<?> unit) {
+         if (name.length() == 1) {
+             return name;                    // Most common cases are "x", "y", "z", "t", "i" and "j".
+         }
+         /*
+          * Direction may be both "compass" (e.g. "North") or "non-compass" (e.g. "Away from").
+          * Even if the radius at θ = 0° is oriented toward North, we do not want the "N" abbreviation.
+          */
+         if (contains(name, "radius", true)) {
+             return "r";
+         }
+         if (isCompass(direction)) {
+             /*
+              * NORTH, EAST, SOUTH, WEST and all intercardinal directions (SOUTH_SOUTH_WEST, etc.):
+              * we will use the acronym (e.g. "SE" for SOUTH_EAST), unless the axis is likely to be
+              * a longitude or latitude axis. We detect those later cases by the unit of measurement.
+              */
+             if (!isIntercardinal(direction) && Units.isAngular(unit)) {
+                 if (contains(name, "Spherical", false)) {
+                     return NORTH.equals(absolute(direction)) ? "Ω" : "θ";
+                 } else {
+                     return NORTH.equals(absolute(direction)) ? "φ" : "λ";
+                 }
+             }
+         } else {
+             /*
+              * All cases other than NORTH, SOUTH, etc. The most common direction is UP, in which case the
+              * abbreviation shall be "H" for Gravity-related height and "h" for ellipsoidal height. Those
+              * two names are specified by ISO 19111, but we will use "Gravity" as a criterion because we
+              * use "h" as the fallback for unknown vertical axis.
+              */
+             if (UP.equals(direction)) {
+                 if (Units.isAngular(unit))               return "α";                // Elevation angle
+                 if (contains(name, "Gravity",    false)) return "H";                // Gravity-related height
+                 if (contains(name, "Geocentric", false)) return "r";                // Geocentric radius
+                 return "h";                                                         // Ellipsoidal height
+             } else if (DOWN.equals(direction)) {
+                 return "D";                                                         // Depth
+             } else if (isGeocentric(direction)) {
+                 // For GEOCENTRIC_X, GEOCENTRIC_Y or GEOCENTRIC_Z, just take the last letter.
+                 final String dir = direction.name();
+                 return dir.substring(dir.length() - 1).trim();
+             }
+             final String abbreviation = ABBREVIATIONS.get(absolute(direction));
+             if (abbreviation != null) {
+                 return abbreviation;
+             }
+         }
+         final String id = direction.identifier();   // UML identifier, or null if none.
+         return camelCaseToAcronym(id != null ? id : direction.name()).toString().intern();
+     }
+ 
+     /**
+      * Returns an axis direction for the given abbreviation. This method is (partially) the converse
+      * of {@link #suggestAbbreviation(String, AxisDirection, Unit)}. Current implementation does not
+      * recognize all abbreviation generated by above method, but only the main ones.  This method is
+      * defined here for making easier to maintain consistency with {@code suggestAbbreviation(…)}.
+      *
+      * @param  abbreviation  the abbreviation.
+      * @return axis direction for the given abbreviation, or {@code null} if unrecognized.
+      */
+     public static AxisDirection fromAbbreviation(char abbreviation) {
+         if (abbreviation >= 'a' && abbreviation <= 'z') {
+             abbreviation -= ('a' - 'A');                    // To upper case, but only for latin characters.
+         }
+         final AxisDirection dir;
+         switch (abbreviation) {
+             default:                      dir = null;                 break;
+             case 'W':                     dir = AxisDirection.WEST;   break;
+             case 'S':                     dir = AxisDirection.SOUTH;  break;
+             case 'θ': case 'λ': case 'E': dir = AxisDirection.EAST;   break;
+             case 'Ω': case 'φ': case 'N': dir = AxisDirection.NORTH;  break;
+             case 'R': case 'H':           dir = AxisDirection.UP;     break;
+             case 'D':                     dir = AxisDirection.DOWN;   break;
+             case 'T':                     dir = AxisDirection.FUTURE; break;
+         }
+         return dir;
+     }
+ 
+     /**
+      * Builds a coordinate system name from the given array of axes.
+      * This method expects a {@code StringBuilder} pre-filled with the coordinate system name.
+      * The axis directions and abbreviations will be appended after the CS name.
+      * Examples:
+      *
+      * <ul>
+      *   <li>Ellipsoidal CS: North (°), East (°).</li>
+      *   <li>Cartesian CS: East (km), North (km).</li>
+      *   <li>Compound CS: East (km), North (km), Up (m).</li>
+      * </ul>
+      *
+      * @param  buffer  a buffer pre-filled with the name header.
+      * @param  axes    the axes to append in the given buffer.
+      * @return a name for the given coordinate system type and axes.
+      */
+     public static String appendTo(final StringBuilder buffer, final CoordinateSystemAxis[] axes) {
+         String separator = ": ";
+         for (final CoordinateSystemAxis axis : axes) {
+             buffer.append(separator).append(Types.getCodeLabel(axis.getDirection()));
+             separator = ", ";
+             final Unit<?> unit = axis.getUnit();
+             if (unit != null) {
+                 final String symbol = unit.toString();
+                 if (symbol != null && !symbol.isEmpty()) {
+                     buffer.append(" (").append(symbol).append(')');
+                 }
+             }
+         }
+         return buffer.append('.').toString();
+     }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtendedPrecisionMatrix.java
index 0000000000,a35348924b..07f7c25896
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtendedPrecisionMatrix.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ExtendedPrecisionMatrix.java
@@@ -1,0 -1,149 +1,150 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import org.opengis.referencing.operation.Matrix;
+ import org.apache.sis.util.resources.Errors;
+ 
+ 
+ /**
+  * A matrix capable to store elements with extended precision.
+  * Apache SIS uses double-double arithmetic for extended precision,
+  * but we want to hide that implementation details from public API.
+  * The double-double arithmetic is implemented by a specialized {@link Number} subclass.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public interface ExtendedPrecisionMatrix extends Matrix {
+     /**
+      * A sentinel value for {@link org.apache.sis.referencing.operation.matrix.Matrices#create(int, int, Number[])}
+      * meaning that we request an extended precision matrix with all elements initialized to zero.
+      * This is a non-public feature because we try to hide our extended-precision mechanism from the users.
+      */
+     Number[] CREATE_ZERO = new Number[0];
+ 
+     /**
+      * A sentinel value for {@link org.apache.sis.referencing.operation.matrix.Matrices#create(int, int, Number[])}
+      * meaning that we request an extended precision matrix initialized to the identity (or diagonal) matrix.
+      * This is a non-public feature because we try to hide our extended-precision mechanism from the users.
+      */
+     Number[] CREATE_IDENTITY = new Number[0];
+ 
+     /**
+      * Returns {@code true} if the given number is zero.
+      * This is the criterion used for identifying which {@link Number} elements shall be {@code null}.
+      *
+      * @param  element  the value to test (may be {@code null}).
+      * @return whether the given value is zero or null.
+      */
+     static boolean isZero(final Number element) {
+         return (element == null) || element.doubleValue() == 0;
+     }
+ 
+     /**
+      * {@return an extended-precision view of the given matrix}.
+      * The returned matrix should be assumed read-only.
+      *
+      * @param m  the matrix to cast or wrap.
+      *
+      * @see org.apache.sis.referencing.operation.matrix.MatrixSIS#asExtendedPrecision(Matrix)
+      */
+     @SuppressWarnings("CloneDoesntCallSuperClone")
+     static ExtendedPrecisionMatrix castOrWrap(final Matrix m) {
+         if (m == null || m instanceof ExtendedPrecisionMatrix) {
+             return (ExtendedPrecisionMatrix) m;
+         }
+         return new ExtendedPrecisionMatrix() {
+             @Override public Number getElementOrNull(int j, int i) {
+                 final double v = m.getElement(j, i);
+                 return (v != 0) ? v : null;
+             }
+ 
+             @Override public double getElement(int j, int i) {return m.getElement(j, i);}
+             @Override public int    getNumRow()              {return m.getNumRow();}
+             @Override public int    getNumCol()              {return m.getNumCol();}
+             @Override public String toString()               {return m.toString();}
+             @Override public Matrix clone()                  {return m.clone();}
++            @Override public boolean isIdentity()            {return m.isIdentity();}
+         };
+     }
+ 
+     /**
+      * Returns all matrix elements in a flat, row-major (column indices vary fastest) array.
+      * The array length is <code>{@linkplain #getNumRow()} * {@linkplain #getNumCol()}</code>.
+      * Zero values <em>shall</em> be null.
+      *
+      * <p>The {@code writable} argument shall be {@code true} if the caller may write in the returned array.
+      * If {@code false}, then the returned array shall be considered read-only because it may be a reference
+      * to the array stored internally by this matrix.</p>
+      *
+      * <p>Note that if the {@code writable} argument is {@code false}, then the values of the returned array
+      * may change if the matrix is mutable. If the caller need a stable array, use {@code true}.</p>
+      *
+      * @param  writable  whether the caller may write in the returned array.
+      * @return a copy of all current matrix elements in a row-major array.
+      */
+     default Number[] getElementAsNumbers(final boolean writable) {
+         final int numCol = getNumCol();
+         final Number[] elements = new Number[getNumRow() * numCol];
+         for (int i=0; i<elements.length; i++) {
+             elements[i] = getElementOrNull(i / numCol, i % numCol);
+         }
+         return elements;
+     }
+ 
+     /**
+      * Retrieves the value at the specified row and column if different than zero.
+      * If the value is zero, then this method <em>shall</em> return {@code null}.
+      * The use of {@code null} for zero is a way to identify zero easily no matter
+      * the value type.
+      *
+      * @param  row     the row index, from 0 inclusive to {@link #getNumRow()} exclusive.
+      * @param  column  the column index, from 0 inclusive to {@link #getNumCol()} exclusive.
+      * @return the current value at the given row and column, or {@code null} if the value is zero.
+      * @throws IndexOutOfBoundsException if the specified row or column is out of bounds.
+      */
+     Number getElementOrNull(int row, int column);
+ 
+     /**
+      * Retrieves the value at the specified row and column of this matrix.
+      *
+      * @param  row     the row index, from 0 inclusive to {@link #getNumRow()} exclusive.
+      * @param  column  the column index, from 0 inclusive to {@link #getNumCol()} exclusive.
+      * @return the current value at the given row and column.
+      * @throws IndexOutOfBoundsException if the specified row or column is out of bounds.
+      */
+     @Override
+     default double getElement(int row, int column) {
+         final Number value = getElementOrNull(row, column);
+         return (value != null) ? value.doubleValue() : 0;
+     }
+ 
+     /**
+      * Modifies the value at the specified row and column of this matrix.
+      * The default implementation assumes that the matrix is unmodifiable.
+      *
+      * @param  row     the row number of the value to set (zero indexed).
+      * @param  column  the column number of the value to set (zero indexed).
+      * @param  value   the new matrix element value.
+      * @throws IndexOutOfBoundsException if the specified row or column is out of bounds.
+      * @throws UnsupportedOperationException if this matrix is unmodifiable.
+      */
+     @Override
+     default void setElement(int row, int column, double value) {
+         throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, getClass()));
+     }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java
index 0000000000,27bf6fbe93..357eb6a265
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/Formulas.java
@@@ -1,0 -1,292 +1,292 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import static java.lang.Math.*;
+ import org.opengis.referencing.datum.Ellipsoid;
+ import org.apache.sis.util.Static;
+ import org.apache.sis.measure.Latitude;
+ import org.apache.sis.util.privy.Numerics;
+ import org.apache.sis.system.Configuration;
+ import org.apache.sis.referencing.datum.DefaultEllipsoid;
+ import static org.apache.sis.math.MathFunctions.atanh;
+ import static org.apache.sis.metadata.privy.ReferencingServices.NAUTICAL_MILE;
+ 
+ 
+ /**
+  * Miscellaneous numerical utilities which should not be put in public API.
+  * This class contains methods that depend on hard-coded arbitrary tolerance threshold, and we
+  * do not want to expose publicly those arbitrary values (or at least not in a too direct way).
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class Formulas extends Static {
+     /**
+      * Default tolerance threshold for comparing coordinate values in a projected CRS,
+      * assuming that the unit of measurement is metre. This constant determines also
+      * (indirectly) the minimum accuracy of iterative methods in map projections.
+      *
+      * <h4>Maintenance</h4>
+      * If this value is modified, then all usages of this constant should be verified.
+      * Some usages may need to be compensated. For example, {@code GeodesicsOnEllipsoid}
+      * uses a millimetric precision by dividing the tolerance by 10 or more. We way want
+      * to keep the same precision there even if {@code LINEAR_TOLERANCE} was made smaller.
+      *
+      * @see #ANGULAR_TOLERANCE
+      * @see org.apache.sis.util.privy.Numerics#COMPARISON_THRESHOLD
+      */
+     @Configuration
+     public static final double LINEAR_TOLERANCE = 0.01;
+ 
+     /**
+      * Default tolerance threshold for comparing coordinate values in a geographic CRS,
+      * assuming that the unit of measurement is decimal degrees and using the standard
+      * nautical mile length.
+      *
+      * <p>For a {@link #LINEAR_TOLERANCE} of 1 centimetre, this is slightly less than 1E-7°.</p>
+      *
+      * @see #LINEAR_TOLERANCE
+      * @see org.apache.sis.util.privy.Numerics#COMPARISON_THRESHOLD
+      */
+     @Configuration
+     public static final double ANGULAR_TOLERANCE = LINEAR_TOLERANCE / (NAUTICAL_MILE * 60);
+ 
+     /**
+      * Default tolerance threshold for comparing coordinate values in temporal CRS,
+      * assuming that the unit of measurement is second. Current value is arbitrary
+      * and may change in any future Apache SIS version.
+      */
+     @Configuration
+     public static final double TEMPORAL_TOLERANCE = 60;             // One minute.
+ 
+     /**
+      * The maximal longitude value before normalization if a centimetric precision is desired.
+      * This is about 4×10⁸ degrees.
+      *
+      * @see org.apache.sis.measure.Longitude#normalize(double)
+      */
+     public static final double LONGITUDE_MAX = Numerics.MAX_INTEGER_CONVERTIBLE_TO_DOUBLE/2 * ANGULAR_TOLERANCE;
+ 
+     /**
+      * The length of a <i>Julian year</i> in milliseconds.
+      * From Wikipedia, <q>In astronomy, a Julian year (symbol: <b>a</b>) is a unit of measurement of time
+      * defined as exactly 365.25 days of 86,400 SI seconds each.</q>.
+      *
+      * @see <a href="https://en.wikipedia.org/wiki/Julian_year_%28astronomy%29">Wikipedia: Julian year (astronomy)</a>
+      */
+     public static final long JULIAN_YEAR_LENGTH = 31557600000L;
+ 
+     /**
+      * Maximum number of iterations for iterative computations. Defined in this {@code Formulas} class as a default value,
+      * but some classes may use a derived value (for example twice this amount). This constant is mostly useful for identifying
+      * places where iterations occur.
+      *
+      * <p>Current value has been determined empirically for allowing {@code GeodesicsOnEllipsoidTest} to pass.</p>
+      */
+     @Configuration
+     public static final int MAXIMUM_ITERATIONS = 18;
+ 
+     /**
+      * Whether to use {@link Math#fma(double, double, double)} for performance reasons.
+      * We do not use this flag when the goal is to get better accuracy rather than performance.
+      * Use of FMA brings performance benefits on machines having hardware support,
+      * but come at a high cost on older machines without hardware support.
+      */
+     @Configuration
 -    public static final boolean USE_FMA = true;
++    public static final boolean USE_FMA = false;
+ 
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private Formulas() {
+     }
+ 
+     /**
+      * Returns {@code true} if {@code ymin} is the south pole and {@code ymax} is the north pole.
+      *
+      * @param  ymin  the minimal latitude to test.
+      * @param  ymax  the maximal latitude to test.
+      * @return {@code true} if the given latitudes are south pole to north pole respectively.
+      */
+     public static boolean isPoleToPole(final double ymin, final double ymax) {
+         return abs(ymin - Latitude.MIN_VALUE) <= ANGULAR_TOLERANCE &&
+                abs(ymax - Latitude.MAX_VALUE) <= ANGULAR_TOLERANCE;
+     }
+ 
+     /**
+      * Returns the size of a planet described by the given ellipsoid compared to earth.
+      * This method returns a ratio of given planet authalic radius compared to WGS84.
+      * This can be used for adjusting {@link #LINEAR_TOLERANCE} and {@link #ANGULAR_TOLERANCE} to another planet.
+      *
+      * @param  planet  ellipsoid of the other planet to compare to Earth, or {@code null}.
+      * @return ratio of planet authalic radius on WGS84 authalic radius, or {@code NaN} if the given ellipsoid is null.
+      */
+     public static double scaleComparedToEarth(final Ellipsoid planet) {
+         return getAuthalicRadius(planet) / 6371007.180918474;
+     }
+ 
+     /**
+      * Returns the radius of a hypothetical sphere having the same surface as the given ellipsoid.
+      *
+      * @param  ellipsoid  the ellipsoid for which to get the radius, or {@code null}.
+      * @return the authalic radius, or {@link Double#NaN} if the given ellipsoid is null.
+      */
+     public static double getAuthalicRadius(final Ellipsoid ellipsoid) {
+         if (ellipsoid == null) {
+             return Double.NaN;
+         } else if (ellipsoid instanceof DefaultEllipsoid) {
+             return ((DefaultEllipsoid) ellipsoid).getAuthalicRadius();      // Give a chance to subclasses to override.
+         } else {
+             return getAuthalicRadius(ellipsoid.getSemiMajorAxis(),
+                                      ellipsoid.getSemiMinorAxis());
+         }
+     }
+ 
+     /**
+      * Returns the radius of a hypothetical sphere having the same surface as the ellipsoid
+      * specified by the given axis length.
+      *
+      * @param  a  the semi-major axis length.
+      * @param  b  the semi-minor axis length.
+      * @return the radius of a sphere having the same surface as the specified ellipsoid.
+      *
+      * @see org.apache.sis.referencing.datum.DefaultEllipsoid#getAuthalicRadius()
+      */
+     public static double getAuthalicRadius(final double a, final double b) {
+         if (a != b) {
+             final double f = 1 - b/a;
+             final double e = sqrt(2*f - f*f);
+             return sqrt(0.5 * (a*a + b*b*atanh(e)/e));
+         } else {
+             return a;
+         }
+     }
+ 
+     /**
+      * Returns the radius of the conformal sphere at a given latitude.
+      * The radius of conformal sphere is computed as below:
+      *
+      * <blockquote>Rc = √(1 – ℯ²) / (1 – ℯ²sin²φ)  where  ℯ² = 1 - (b/a)²</blockquote>
+      *
+      * This is a function of latitude and therefore not constant.
+      *
+      * @param  ellipsoid  the ellipsoid for which to compute the radius of conformal sphere.
+      * @param  φ          the latitude in radians where to compute the radius of conformal sphere.
+      * @return radius of the conformal sphere at latitude φ.
+      */
+     public static double radiusOfConformalSphere(final Ellipsoid ellipsoid, final double φ) {
+         final double sinφ = Math.sin(φ);
+         final double a = ellipsoid.getSemiMajorAxis();
+         final double r = ellipsoid.getSemiMinorAxis() / a;
+         return a * (r / (1 - (1 - r*r) * (sinφ*sinφ)));
+     }
+ 
+     /**
+      * Returns the geocentric radius at the given latitude.
+      *
+      * @param  ellipsoid  the ellipsoid for which to compute the radius.
+      * @param  φ          the latitude in radians where to compute the radius.
+      * @return radius at latitude φ.
+      *
+      * @see <a href="https://en.wikipedia.org/wiki/Earth_radius#Geocentric_radius">Geocentric radius on Wikipedia</a>
+      */
+     public static double geocentricRadius(final Ellipsoid ellipsoid, final double φ) {
+         final double a  = ellipsoid.getSemiMajorAxis();
+         final double b  = ellipsoid.getSemiMinorAxis();
+         double at = a * Math.cos(φ); at *= at;
+         double bt = b * Math.sin(φ); bt *= bt;
+         return Math.sqrt((a*a*at + b*b*bt) / (at + bt));
+     }
+ 
+     /**
+      * Computes the semi-minor axis length from the given semi-major axis and inverse flattening factor.
+      *
+      * @param  semiMajorAxis      the semi-major axis length.
+      * @param  inverseFlattening  the inverse flattening factor.
+      * @return the semi-minor axis length.
+      */
+     public static double getSemiMinor(final double semiMajorAxis, final double inverseFlattening) {
+         /*
+          * Note: double-double arithmetic does not increase the accuracy here, unless the inverse flattening
+          * factor given to this method is very high (i.e. the planet is very close to a perfect sphere).
+          */
+         return semiMajorAxis * (1 - 1/inverseFlattening);
+     }
+ 
+     /**
+      * Computes the inverse flattening factor from the given axis lengths.
+      *
+      * @param  semiMajorAxis  the semi-major axis length.
+      * @param  semiMinorAxis  the semi-minor axis length.
+      * @return the inverse flattening factor.
+      */
+     public static double getInverseFlattening(final double semiMajorAxis, final double semiMinorAxis) {
+         /*
+          * Note: double-double arithmetic here sometimes change the last digit. We ignore for now.
+          * We may consider using double-double arithmetic in a future SIS version, not for more
+          * accurate map projection but rather for being able to find back the original value after
+          * we convert back and forward betwen inverse flattening and semi-minor axis length.
+          */
+         return semiMajorAxis / (semiMajorAxis - semiMinorAxis);
+     }
+ 
+     /**
+      * Returns {@code sqrt(x² + y²)} for coordinate values on an ellipsoid of semi-major axis length of 1.
+      * This method does not provides the accuracy guarantees offered by {@link Math#hypot(double, double)}.
+      * However for values close to 1, this approximation seems to stay within 1 ULP of {@code Math.hypot(…)}.
+      * We tested with random values in ranges up to [-6 … +6].
+      *
+      * <p>We define this method because {@link Math#hypot(double, double)} has been measured with JMH as 6 times
+      * slower than {@link Math#sqrt(double)} on Java 14.  According posts on internet, the same performance cost
+      * is observed in C/C++ too. Despite its cost, {@code hypot(…)} is generally recommended because computing a
+      * hypotenuse from large magnitudes has accuracy problems. But in the context of {@code NormalizedProjection}
+      * where semi-axis lengths are close to 1, input values should be (x,y) coordinates in the [−1 … +1] range.
+      * The actual range may be greater (e.g. [−5 … +5]), but it still far from ranges requiring protection against
+      * overflow.</p>
+      *
+      * <h4>Caution</h4>
+      * We may not need the full {@code Math.hypot(x,y)} accuracy in the context of map projections on ellipsoids.
+      * However, some projection formulas require that {@code fastHypot(x,y) ≥ max(|x|,|y|)}, otherwise normalizations
+      * such as {@code x/hypot(x,y)} could result in values larger than 1, which in turn result in {@link Double#NaN}
+      * when given to {@link Math#asin(double)}. The assumption on x, y and {@code sqrt(x²+y²)} relative magnitude is
+      * broken when x=0 and |y| ≤ 1.4914711209038602E-154 or conversely. This method does not check for such cases;
+      * it is caller responsibility to add this check is necessary, for example as below:
+      *
+      * {@snippet lang="java" :
+      *     double D = max(fastHypot(x, y), max(abs(x), abs(y)));
+      *     }
+      *
+      * According JMH, above check is 1.65 time slower than {@code fastHypot} without checks.
+      * We define this {@code fastHypot(…)} method for tracing where {@code sqrt(x² + y²)} is used,
+      * so we can verify if it is used in context where the inaccuracy is acceptable.
+      *
+      * <h4>When to use</h4>
+      * We reserve this method to ellipsoidal formulas where approximations are used anyway. Implementations using
+      * exact formulas, such as spherical formulas, should use {@link Math#hypot(double, double)} for its accuracy.
+      *
+      * @param  x    one side of the triangle. Should be approximately in the [-1 … +1] range.
+      * @param  y  other side of the triangle. Should be approximately in the [-1 … +1] range.
+      * @return hypotenuse, not smaller than {@code max(|x|,|y|)} unless the values are less than 1.5E-154.
+      *
+      * @see <a href="https://issues.apache.org/jira/browse/NUMBERS-143">Investigate Math.hypot for computing the absolute of a complex number</a>
+      * @see <a href="https://scicomp.stackexchange.com/questions/27758/is-there-any-point-to-using-hypot-for-sqrt1c2-0-le-c-le-1-for-real/27766">Is
+      *      there any point to using <code>hypot(1, c)</code> for <code>sqrt(1 + c²)</code>, 0 ≤ c ≤ 1</a>
+      */
+     public static double fastHypot(final double x, final double y) {
+         return sqrt(x*x + y*y);
+     }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/NilReferencingObject.java
index 0000000000,5c60061a72..efc6503e34
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/NilReferencingObject.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/NilReferencingObject.java
@@@ -1,0 -1,77 +1,99 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import org.opengis.util.InternationalString;
+ import org.opengis.referencing.ReferenceSystem;
+ import org.apache.sis.xml.NilReason;
+ import org.apache.sis.xml.NilObject;
+ import org.apache.sis.referencing.NamedIdentifier;
+ import org.apache.sis.util.resources.Vocabulary;
+ 
+ // Specific to the main and geoapi-3.1 branches:
+ import org.opengis.referencing.ReferenceIdentifier;
+ 
++// Specific to the main branch:
++import java.util.Set;
++import java.util.Collection;
++import org.opengis.util.GenericName;
++import org.opengis.metadata.extent.Extent;
++import org.apache.sis.io.wkt.UnformattableObjectException;
++
+ 
+ /**
+  * A referencing object for which every methods return {@code null} or a neutral value.
+  * <strong>This is not a valid object</strong>. It is used only for initialization of
+  * objects to be used by JAXB at unmarshalling time, as a way to simulate "no-argument"
+  * constructor required by JAXB.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class NilReferencingObject implements NilObject, ReferenceSystem {
+     /**
+      * The default name of {@code NilReferencingObject} instances.
+      * We use this value because {@link ReferenceSystem#getName()}
+      * is a mandatory property and not all code is tolerant to null name.
+      *
+      * <h4>Usage note</h4>
+      * In theory we do not need a default name because it will be replaced by
+      * the value of the {@code <gml:name>} element anyway at XML unmarshalling time.
+      * But not all XML documents are valid, so the {@code <gml:name>} may be missing.
+      */
+     public static final ReferenceIdentifier UNNAMED = new NamedIdentifier(null, Vocabulary.format(Vocabulary.Keys.Unnamed));
+ 
+     /**
+      * The unique instance.
+      */
+     public static final NilReferencingObject INSTANCE = new NilReferencingObject();
+ 
+     /**
+      * Do not allow other instantiation of {@link #INSTANCE}.
+      */
+     private NilReferencingObject() {
+     }
+ 
+     /**
+      * This object is empty because the value will be provided later.
+      */
+     @Override
+     public NilReason getNilReason() {
+         return NilReason.TEMPLATE;
+     }
+ 
+     /*
+      * Simple properties. Not all of them are optional, but SIS is tolerant to null values.
+      * Returning null for collection are okay in the particular case of SIS implementation,
+      * because the constructor will replace empty collections by null references anyway.
+      */
 -    @Override public ReferenceIdentifier getName()  {return UNNAMED;}
 -    @Override public InternationalString getScope() {return null;}
++    @Override public ReferenceIdentifier      getName()        {return UNNAMED;}
++    @Override public Collection<GenericName>  getAlias()       {return null;}
++    @Override public Set<ReferenceIdentifier> getIdentifiers() {return null;}
++    @Override public InternationalString      getRemarks()     {return null;}
++    @Override public InternationalString      getScope()       {return null;}
++    @Override public Extent getDomainOfValidity()              {return null;}
++
++    /**
++     * Throws the exception in all cases.
++     *
++     * @return never return.
++     * @throws UnformattableObjectException always thrown.
++     */
++    @Override
++    public String toWKT() throws UnformattableObjectException {
++        throw new UnformattableObjectException();
++    }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/PositionalAccuracyConstant.java
index 0000000000,7370c6c5e6..81c656787d
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/PositionalAccuracyConstant.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/PositionalAccuracyConstant.java
@@@ -1,0 -1,246 +1,246 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import java.util.Set;
+ import java.util.Collection;
+ import java.io.ObjectStreamException;
+ import jakarta.xml.bind.annotation.XmlTransient;
+ import javax.measure.Unit;
+ import javax.measure.quantity.Length;
+ import org.opengis.util.Record;
+ import org.opengis.util.InternationalString;
+ import org.opengis.metadata.quality.PositionalAccuracy;
+ import org.opengis.metadata.quality.EvaluationMethodType;
+ import org.opengis.metadata.quality.QuantitativeResult;
+ import org.opengis.metadata.quality.Result;
+ import org.opengis.referencing.operation.ConcatenatedOperation;
+ import org.opengis.referencing.operation.Conversion;
+ import org.opengis.referencing.operation.CoordinateOperation;
+ import org.opengis.referencing.operation.Transformation;
+ import org.apache.sis.measure.Units;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.metadata.iso.quality.DefaultConformanceResult;
+ import org.apache.sis.metadata.iso.quality.DefaultAbsoluteExternalPositionalAccuracy;
+ import org.apache.sis.system.Configuration;
+ import org.apache.sis.referencing.internal.Resources;
+ import org.apache.sis.util.resources.Vocabulary;
+ 
+ 
+ /**
+  * Predefined positional accuracy resulting from some coordinate operations.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  *
+  * @see org.opengis.referencing.operation.Transformation#getCoordinateOperationAccuracy()
+  */
+ @XmlTransient
+ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPositionalAccuracy {
+     /**
+      * Serial number for inter-operability with different versions.
+      */
+     private static final long serialVersionUID = -2554090935254116470L;
+ 
+     /**
+      * Presumed worst case error when no datum shift information was found.
+      * The highest value found in the EPSG database 6.7 is 999 metres (worst datum shift), so this error
+      * should be yet higher. I have seen 3 kilometres mentioned in some documentation somewhere.
+      *
+      * <p>If this value is modified, please update {@code getLinearAccuracy()} public javadoc accordingly.</p>
+      *
+      * @see org.apache.sis.referencing.operation.AbstractCoordinateOperation#getLinearAccuracy()
+      */
+     @Configuration
+     public static final double UNKNOWN_ACCURACY = 3000;
+ 
+     /**
+      * Default accuracy of datum shift, if not explicitly provided in the EPSG database.
+      * The 25 meters value is the next highest value (after 999 metres) found in the EPSG
+      * database version 6.7 for a significant number of transformations.
+      *
+      * <p>If this value is modified, please update {@code getLinearAccuracy()} public javadoc accordingly.</p>
+      *
+      * @see org.apache.sis.referencing.operation.AbstractCoordinateOperation#getLinearAccuracy()
+      */
+     @Configuration
+     private static final double DATUM_SHIFT_ACCURACY = 25;
+ 
+     /**
+      * Default accuracy of datum shifts when using an intermediate datum (typically WGS 84).
+      * Since this is a concatenation of two datum shifts, we use twice {@link #DATUM_SHIFT_ACCURACY}.
+      * The result is multiplied by 2 again as a margin because we have no guarantees that the domain
+      * of validity of the two datum are close enough for making this concatenation valid.
+      */
+     @Configuration
+     public static final double INDIRECT_SHIFT_ACCURACY = 100;
+ 
+     /**
+      * Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
+      * requires a datum shift and some method has been applied. Datum shift methods often use
+      * {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa Wolf parameters},
+      * but other kind of method may have been applied as well.
+      */
+     public static final PositionalAccuracy DATUM_SHIFT_APPLIED;
+ 
+     /**
+      * Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
+      * requires a datum shift, but no method has been found applicable. This usually means that no
+      * {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa Wolf parameters} have
+      * been found. Such datum shifts are approximations and may have 1 kilometer error.
+      */
+     public static final PositionalAccuracy DATUM_SHIFT_OMITTED;
+ 
+     /**
+      * Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
+      * requires a datum shift, but only an indirect method has been found. The indirect method uses
+      * an intermediate datum, typically WGS 84.
+      */
+     public static final PositionalAccuracy INDIRECT_SHIFT_APPLIED;
+     static {
+         final InternationalString desc = Vocabulary.formatInternational(Vocabulary.Keys.TransformationAccuracy);
+         final InternationalString eval = Resources .formatInternational(Resources.Keys.ConformanceMeansDatumShift);
+         DATUM_SHIFT_APPLIED    = new PositionalAccuracyConstant(desc, eval, true);
+         DATUM_SHIFT_OMITTED    = new PositionalAccuracyConstant(desc, eval, false);
+         INDIRECT_SHIFT_APPLIED = new PositionalAccuracyConstant(desc, eval, true);
+     }
+ 
+     /**
+      * Creates an positional accuracy initialized to the given result.
+      */
+     @SuppressWarnings("deprecation")
+     private PositionalAccuracyConstant(final InternationalString measureDescription,
+             final InternationalString evaluationMethodDescription, final boolean pass)
+     {
+         DefaultConformanceResult result = new DefaultConformanceResult(Citations.SIS, evaluationMethodDescription, pass);
+         setResults(Set.of(result));
+         setMeasureDescription(measureDescription);
+         setEvaluationMethodDescription(evaluationMethodDescription);
+         setEvaluationMethodType(EvaluationMethodType.DIRECT_INTERNAL);
+         transitionTo(State.FINAL);
+     }
+ 
+     /**
+      * Invoked on deserialization. Replace this instance by one of the constants, if applicable.
+      *
+      * @return the object to use after deserialization.
+      * @throws ObjectStreamException if the serialized object defines an unknown data type.
+      */
+     private Object readResolve() throws ObjectStreamException {
+         if (equals(DATUM_SHIFT_APPLIED))    return DATUM_SHIFT_APPLIED;
+         if (equals(DATUM_SHIFT_OMITTED))    return DATUM_SHIFT_OMITTED;
+         if (equals(INDIRECT_SHIFT_APPLIED)) return INDIRECT_SHIFT_APPLIED;
+         return this;
+     }
+ 
+     /**
+      * Convenience method returning the accuracy in meters for the specified operation.
+      * This method tries each of the following procedures and returns the first successful one:
+      *
+      * <ul>
+      *   <li>If at least one {@link QuantitativeResult} is found with a linear unit, then the largest
+      *       accuracy estimate is converted to {@linkplain Units#METRE metres} and returned.</li>
+      *   <li>Otherwise, if the operation is a {@link Conversion}, then returns 0 since a conversion
+      *       is by definition accurate up to rounding errors.</li>
+      *   <li>Otherwise, if the operation is a {@link Transformation}, then checks if the datum shift
+      *       were applied with the help of Bursa-Wolf parameters. This procedure looks for SIS-specific
+      *       {@link #DATUM_SHIFT_APPLIED} and {@link #DATUM_SHIFT_OMITTED DATUM_SHIFT_OMITTED} constants.</li>
+      *   <li>Otherwise, if the operation is a {@link ConcatenatedOperation}, returns the sum of the accuracy
+      *       of all components. This is a conservative scenario where we assume that errors cumulate linearly.
+      *       Note that this is not necessarily the "worst case" scenario since the accuracy could be worst
+      *       if the math transforms are highly non-linear.</li>
+      * </ul>
+      *
+      * If the above is modified, please update {@code AbstractCoordinateOperation.getLinearAccuracy()} javadoc.
+      *
+      * @param  operation  the operation to inspect for accuracy.
+      * @return the accuracy estimate (always in meters), or NaN if unknown.
+      *
+      * @see org.apache.sis.referencing.operation.AbstractCoordinateOperation#getLinearAccuracy()
+      */
+     public static double getLinearAccuracy(final CoordinateOperation operation) {
+         double accuracy = Double.NaN;
+         final Collection<PositionalAccuracy> accuracies = operation.getCoordinateOperationAccuracy();
+         for (final PositionalAccuracy metadata : accuracies) {
+             for (final Result result : metadata.getResults()) {
+                 if (result instanceof QuantitativeResult) {
+                     final QuantitativeResult quantity = (QuantitativeResult) result;
+                     final Collection<? extends Record> records = quantity.getValues();
+                     if (records != null) {
+                         final Unit<?> unit = quantity.getValueUnit();
+                         if (Units.isLinear(unit)) {
+                             final Unit<Length> unitOfLength = unit.asType(Length.class);
+                             for (final Record record : records) {
 -                                for (final Object value : record.getFields().values()) {
++                                for (final Object value : record.getAttributes().values()) {
+                                     if (value instanceof Number) {
+                                         double v = ((Number) value).doubleValue();
+                                         v = unitOfLength.getConverterTo(Units.METRE).convert(v);
+                                         if (v >= 0 && !(v <= accuracy)) {       // '!' is for replacing the NaN value.
+                                             accuracy = v;
+                                         }
+                                     }
+                                 }
+                             }
+                         }
+                     }
+                 }
+             }
+         }
+         if (Double.isNaN(accuracy)) {
+             /*
+              * No quantitative (linear) accuracy were found. If the coordinate operation is actually
+              * a conversion, the accuracy is up to rounding error (i.e. conceptually 0) by definition.
+              */
+             if (operation instanceof Conversion) {
+                 return 0;
+             }
+             /*
+              * If the coordinate operation is actually a transformation, checks if Bursa-Wolf parameters
+              * were available for the datum shift. This is SIS-specific. See field javadoc for a rational
+              * about the return values chosen.
+              */
+             if (operation instanceof Transformation) {
+                 for (final PositionalAccuracy element : accuracies) {
+                     /*
+                      * Really need identity comparisons, not Object.equals(Object), because the latter
+                      * does not distinguish between DATUM_SHIFT_APPLIED and INDIRECT_SHIFT_APPLIED.
+                      */
+                     if (element == DATUM_SHIFT_APPLIED)    return DATUM_SHIFT_ACCURACY;
+                     if (element == DATUM_SHIFT_OMITTED)    return UNKNOWN_ACCURACY;
+                     if (element == INDIRECT_SHIFT_APPLIED) return INDIRECT_SHIFT_ACCURACY;
+                 }
+             }
+             /*
+              * If the coordinate operation is a compound of other coordinate operations, returns the sum of their accuracy,
+              * skipping unknown ones. Making the sum is a conservative approach (not exactly the "worst case" scenario,
+              * since it could be worst if the transforms are highly non-linear).
+              */
+             if (operation instanceof ConcatenatedOperation) {
+                 for (final CoordinateOperation op : ((ConcatenatedOperation) operation).getOperations()) {
+                     final double candidate = Math.abs(getLinearAccuracy(op));
+                     if (!Double.isNaN(candidate)) {
+                         if (Double.isNaN(accuracy)) {
+                             accuracy = candidate;
+                         } else {
+                             accuracy += candidate;
+                         }
+                     }
+                 }
+             }
+         }
+         return accuracy;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingFactoryContainer.java
index 0000000000,aa9fbde384..c0007a6018
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingFactoryContainer.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingFactoryContainer.java
@@@ -1,0 -1,398 +1,403 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import java.util.Map;
+ import org.opengis.util.FactoryException;
+ import org.opengis.util.NameFactory;
+ import org.opengis.referencing.cs.CartesianCS;
+ import org.opengis.referencing.cs.CSFactory;
+ import org.opengis.referencing.cs.CSAuthorityFactory;
+ import org.opengis.referencing.crs.CRSFactory;
+ import org.opengis.referencing.crs.CRSAuthorityFactory;
+ import org.opengis.referencing.datum.DatumFactory;
+ import org.opengis.referencing.datum.DatumAuthorityFactory;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.opengis.referencing.operation.CoordinateOperationFactory;
+ import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
+ import org.apache.sis.referencing.CRS;
+ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+ import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
+ import org.apache.sis.referencing.factory.GeodeticObjectFactory;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.iso.DefaultNameFactory;
+ import org.apache.sis.util.resources.Errors;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.Factory;
++// Specific to the main branch:
++import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
+ 
+ 
+ /**
+  * A container of factories frequently used together.
+  * Provides also some utility methods working with factories.
+  *
+  * This class may be temporary until we choose a dependency injection framework
+  * See <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>.
+  *
+  * <p>This class is not thread safe. Synchronization, if needed, is caller's responsibility.</p>
+  *
+  * @author  Martin Desruisseaux (IRD, Geomatys)
+  *
+  * @see <a href="https://issues.apache.org/jira/browse/SIS-102">SIS-102</a>
+  */
+ public class ReferencingFactoryContainer {
+     /**
+      * The key for specifying a {@link NameFactory} instance to use for geodetic object constructions.
+      */
+     public static final String NAME_FACTORY = "nameFactory";
+ 
+     /**
+      * The key for specifying a {@link DatumFactory} instance to use for geodetic object constructions.
+      */
+     public static final String DATUM_FACTORY = "datumFactory";
+ 
+     /**
+      * The key for specifying a {@link CSFactory} instance to use for geodetic object constructions.
+      */
+     public static final String CS_FACTORY = "csFactory";
+ 
+     /**
+      * The key for specifying a {@link CRSFactory} instance to use for geodetic object constructions.
+      */
+     public static final String CRS_FACTORY = "crsFactory";
+ 
+     /**
+      * The key for specifying a {@link CoordinateOperationFactory} instance to use for geodetic object constructions.
+      */
+     public static final String OPERATION_FACTORY = "copFactory";
+ 
+     /**
+      * The key for specifying a {@link MathTransformFactory} instance to use for geodetic object constructions.
+      * This is usually not needed for CRS construction, except in the special case of a derived CRS created
+      * from a defining conversion.
+      */
+     public static final String MT_FACTORY = "mtFactory";
+ 
+     /**
+      * The factory for creating coordinate reference systems from authority codes.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private CRSAuthorityFactory crsAuthorityFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.util.GenericName name} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private NameFactory nameFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.datum.Datum datum} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private DatumFactory datumFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.cs.CoordinateSystem coordinate system} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private CSFactory csFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.crs.CoordinateReferenceSystem coordinate reference system} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private CRSFactory crsFactory;
+ 
+     /**
+      * Factory for fetching operation methods and creating defining conversions.
+      * This is needed only for user-defined projected coordinate reference system.
+      */
 -    private CoordinateOperationFactory operationFactory;
++    private DefaultCoordinateOperationFactory operationFactory;
+ 
+     /**
+      * The {@linkplain org.opengis.referencing.operation.MathTransform math transform} factory.
+      * If null, then a default factory will be created only when first needed.
+      */
+     private MathTransformFactory mtFactory;
+ 
+     /**
+      * 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;
+ 
+     /**
+      * Creates a new instance for the default factories.
+      */
+     public ReferencingFactoryContainer() {
+     }
+ 
+     /**
+      * Creates a new instance with factories fetched from the given map of properties.
+      * Factories that are not present in the map will be left to their default value.
+      * This method recognizes the keys declared as static {@link String} constants in this class.
+      * Other entries are ignored.
+      *
+      * @param  properties  the factories.
+      */
+     public ReferencingFactoryContainer(final Map<String,?> properties) {
+         nameFactory      = (NameFactory)                properties.get(NAME_FACTORY);
+         datumFactory     = (DatumFactory)               properties.get(DATUM_FACTORY);
+         csFactory        = (CSFactory)                  properties.get(CS_FACTORY);
+         crsFactory       = (CRSFactory)                 properties.get(CRS_FACTORY);
 -        operationFactory = (CoordinateOperationFactory) properties.get(OPERATION_FACTORY);
++        operationFactory = (DefaultCoordinateOperationFactory) properties.get(OPERATION_FACTORY);
+         mtFactory        = (MathTransformFactory)       properties.get(MT_FACTORY);
+     }
+ 
+     /**
+      * Creates a new instance which will use the given factories.
+      * Any factory given in argument may be {@code null} if lazy instantiation is desired.
+      *
+      * @param  defaultProperties  default properties to give to the objects to create (will not be cloned).
+      * @param  crsFactory         the factory to use for creating coordinate reference systems.
+      * @param  csFactory          the factory to use for creating coordinate systems.
+      * @param  datumFactory       the factory to use for creating datum.
+      * @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,
+                                        final CRSFactory                 crsFactory,
+                                        final CSFactory                  csFactory,
+                                        final DatumFactory               datumFactory,
+                                        final CoordinateOperationFactory operationFactory,
+                                        final MathTransformFactory       mtFactory)
+     {
+         this.defaultProperties = defaultProperties;
+         this.crsFactory        = crsFactory;
+         this.csFactory         = csFactory;
+         this.datumFactory      = datumFactory;
 -        this.operationFactory  = operationFactory;
++        this.operationFactory  = (DefaultCoordinateOperationFactory) operationFactory;      // Because of GeoAPI 3.0 limitation.
+         this.mtFactory         = mtFactory;
+     }
+ 
+     /**
+      * Sets one of the factories managed by this container.
+      * The given {@code type} argument can be one of the following values:
+      *
+      * <ul>
+      *   <li><code>{@linkplain NameFactory}.class</code></li>
+      *   <li><code>{@linkplain DatumFactory}.class</code></li>
+      *   <li><code>{@linkplain CSFactory}.class</code></li>
+      *   <li><code>{@linkplain CRSFactory}.class</code></li>
+      *   <li><code>{@linkplain CoordinateOperationFactory}.class</code></li>
+      *   <li><code>{@linkplain MathTransformFactory}.class</code></li>
+      * </ul>
+      *
+      * Note that authority factories are not yet handled by this method
+      * for consistency with {@link #getFactory(Class)}.
+      *
+      * @param  <T>      the compile-time type of the {@code type} argument.
+      * @param  type     the factory type.
+      * @param  factory  the factory to use for the given type, or {@code null} for the default.
+      * @return {@code true} if the factory changed as a result of this method call.
+      * @throws IllegalArgumentException if the {@code type} argument is not one of the valid values.
+      */
 -    public final <T extends Factory> boolean setFactory(final Class<T> type, final T factory) {
++    public final <T> boolean setFactory(final Class<T> type, final T factory) {
+         if (type == NameFactory.class)                return nameFactory      != (nameFactory      = (NameFactory)                factory);
+         if (type == DatumFactory.class)               return datumFactory     != (datumFactory     = (DatumFactory)               factory);
+         if (type == CSFactory.class)                  return csFactory        != (csFactory        = (CSFactory)                  factory);
+         if (type == CRSFactory.class)                 return crsFactory       != (crsFactory       = (CRSFactory)                 factory);
 -        if (type == CoordinateOperationFactory.class) return operationFactory != (operationFactory = (CoordinateOperationFactory) factory);
++        if (type == CoordinateOperationFactory.class) return operationFactory != (operationFactory = (DefaultCoordinateOperationFactory) factory);
+         if (type == MathTransformFactory.class)       return mtFactory        != (mtFactory        = (MathTransformFactory)       factory);
+         throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
+     }
+ 
+     /**
+      * Returns one of the factories managed by this container.
+      * The given {@code type} argument can be one of the following values:
+      *
+      * <ul>
+      *   <li><code>{@linkplain NameFactory}.class</code></li>
+      *   <li><code>{@linkplain DatumFactory}.class</code></li>
+      *   <li><code>{@linkplain CSFactory}.class</code></li>
+      *   <li><code>{@linkplain CRSFactory}.class</code></li>
+      *   <li><code>{@linkplain CoordinateOperationFactory}.class</code></li>
+      *   <li><code>{@linkplain MathTransformFactory}.class</code></li>
+      * </ul>
+      *
+      * Note that authority factories are not yet handled by this method, since we would have to expose
+      * the current restriction to EPSG geodetic dataset and to handle {@link FactoryException}.
+      *
+      * @param  <T>   the compile-time type of the {@code type} argument.
+      * @param  type  the factory type.
+      * @return the factory for the given type.
+      * @throws IllegalArgumentException if the {@code type} argument is not one of the valid values.
+      */
 -    public final <T extends Factory> T getFactory(final Class<T> type) {
 -        final Factory f;
++    public final <T> T getFactory(final Class<T> type) {
++        final Object f;
+              if (type == NameFactory.class)                f = getNameFactory();
+         else if (type == DatumFactory.class)               f = getDatumFactory();
+         else if (type == CSFactory.class)                  f = getCSFactory();
+         else if (type == CRSFactory.class)                 f = getCRSFactory();
+         else if (type == CoordinateOperationFactory.class) f = getCoordinateOperationFactory();
+         else if (type == MathTransformFactory.class)       f = getMathTransformFactory();
+         else {
+             throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
+         }
+         return type.cast(f);
+     }
+ 
+     /**
+      * Returns the factory for creating generic name.
+      *
+      * @return the Generic Name factory (never {@code null}).
+      */
+     public final NameFactory getNameFactory() {
+         if (nameFactory == null) {
+             nameFactory = DefaultNameFactory.provider();
+         }
+         return nameFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating datum, prime meridians and ellipsoids.
+      *
+      * @return the Datum factory (never {@code null}).
+      */
+     public final DatumFactory getDatumFactory() {
+         if (datumFactory == null) {
+             datumFactory = GeodeticObjectFactory.provider();
+         }
+         return datumFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate systems and their axes.
+      *
+      * @return the Coordinate System factory (never {@code null}).
+      */
+     public final CSFactory getCSFactory() {
+         if (csFactory == null) {
+             csFactory = GeodeticObjectFactory.provider();
+         }
+         return csFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate reference systems.
+      *
+      * @return the Coordinate Reference System factory (never {@code null}).
+      */
+     public final CRSFactory getCRSFactory() {
+         if (crsFactory == null) {
+             crsFactory = GeodeticObjectFactory.provider();
+         }
+         return crsFactory;
+     }
+ 
+     /**
+      * Returns the factory for fetching operation methods and creating defining conversions.
+      * This is needed only for user-defined projected coordinate reference system.
+      * The factory is fetched when first needed.
+      *
+      * @return the Coordinate Operation factory (never {@code null}).
+      */
 -    public final CoordinateOperationFactory getCoordinateOperationFactory() {
++    public final DefaultCoordinateOperationFactory getCoordinateOperationFactory() {
+         if (operationFactory == null) {
 -            operationFactory = CoordinateOperations.getCoordinateOperationFactory(defaultProperties, mtFactory, crsFactory, csFactory);
++            CoordinateOperationFactory op = CoordinateOperations.getCoordinateOperationFactory(defaultProperties, mtFactory, crsFactory, csFactory);
+             defaultProperties = null;       // Not needed anymore.
++            if (op instanceof DefaultCoordinateOperationFactory) {
++                operationFactory = (DefaultCoordinateOperationFactory) op;
++            } else {
++                operationFactory = DefaultCoordinateOperationFactory.provider();
++            }
+         }
+         return operationFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating parameterized transforms.
+      *
+      * @return the Math Transform factory (never {@code null}).
+      */
+     public final MathTransformFactory getMathTransformFactory() {
+         if (mtFactory == null) {
+             mtFactory = DefaultMathTransformFactory.provider();
+         }
+         return mtFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating datum from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Datum authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory cannot be obtained.
+      */
+     public final DatumAuthorityFactory getDatumAuthorityFactory() throws FactoryException {
+         final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+         if (factory instanceof DatumAuthorityFactory) {                 // This is the case for SIS implementation.
+             return (DatumAuthorityFactory) factory;
+         }
+         throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate systems from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Coordinate System authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory cannot be obtained.
+      */
+     public final CSAuthorityFactory getCSAuthorityFactory() throws FactoryException {
+         final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+         if (factory instanceof CSAuthorityFactory) {                    // This is the case for SIS implementation.
+             return (CSAuthorityFactory) factory;
+         }
+         throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate reference systems from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Coordinate Reference System authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory cannot be obtained.
+      */
+     public final CRSAuthorityFactory getCRSAuthorityFactory() throws FactoryException {
+         if (crsAuthorityFactory == null) {
+             crsAuthorityFactory = CRS.getAuthorityFactory(Constants.EPSG);
+         }
+         return crsAuthorityFactory;
+     }
+ 
+     /**
+      * Returns the factory for creating coordinate operations from authority codes.
+      * Currently only EPSG codes are supported.
+      *
+      * @return the Coordinate Operation authority factory (never {@code null}).
+      * @throws FactoryException if the authority factory cannot be obtained.
+      */
+     public final CoordinateOperationAuthorityFactory getCoordinateOperationAuthorityFactory() throws FactoryException {
+         final CRSAuthorityFactory factory = getCRSAuthorityFactory();
+         if (factory instanceof CoordinateOperationAuthorityFactory) {       // This is the case for SIS implementation.
+             return (CoordinateOperationAuthorityFactory) factory;
+         }
+         throw new NoSuchAuthorityFactoryException(null, Constants.EPSG);
+     }
+ 
+     /**
+      * Returns the a coordinate system for map projections with (easting, northing) axes in metres.
+      * EPSG::4400 — Cartesian 2D CS. Axes: easting, northing (E,N). Orientations: east, north. UoM: m.
+      *
+      * @return a coordinate system with (easting, northing) axes in metres.
+      * @throws FactoryException if an error occurred while creating the coordinate system.
+      */
+     public final CartesianCS getStandardProjectedCS() throws FactoryException {
+         return getCSAuthorityFactory().createCartesianCS("4400");
+     }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
index 0000000000,9a1ca4c79f..af3a37f029
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
@@@ -1,0 -1,684 +1,687 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.Collection;
+ import java.util.NoSuchElementException;
+ import javax.measure.Unit;
+ import javax.measure.quantity.Angle;
+ import org.opengis.annotation.UML;
+ import org.opengis.annotation.Specification;
+ import org.opengis.metadata.Identifier;
+ import org.opengis.metadata.citation.Citation;
+ import org.opengis.parameter.ParameterValueGroup;
+ import org.opengis.parameter.ParameterDescriptorGroup;
+ import org.opengis.parameter.GeneralParameterDescriptor;
+ import org.opengis.util.FactoryException;
+ import org.opengis.referencing.IdentifiedObject;
+ import org.opengis.referencing.cs.*;
+ import org.opengis.referencing.crs.*;
+ import org.opengis.referencing.datum.Datum;
+ import org.opengis.referencing.datum.Ellipsoid;
+ import org.opengis.referencing.datum.PrimeMeridian;
+ import org.opengis.referencing.datum.GeodeticDatum;
+ import org.opengis.referencing.datum.VerticalDatum;
+ import org.opengis.referencing.datum.VerticalDatumType;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.apache.sis.util.Static;
+ import org.apache.sis.util.Classes;
+ import org.apache.sis.util.Utilities;
+ import org.apache.sis.util.CharSequences;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.referencing.CommonCRS;
+ import org.apache.sis.referencing.NamedIdentifier;
+ import org.apache.sis.referencing.IdentifiedObjects;
+ import org.apache.sis.referencing.AbstractIdentifiedObject;
+ import org.apache.sis.referencing.datum.DefaultPrimeMeridian;
+ import org.apache.sis.referencing.crs.DefaultGeographicCRS;
+ import org.apache.sis.referencing.cs.AxesConvention;
+ import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
+ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory.Context;
+ import org.apache.sis.metadata.privy.Identifiers;
+ import org.apache.sis.xml.NilObject;
+ 
++// Specific to the main branch:
++import org.opengis.referencing.ReferenceIdentifier;
++
+ 
+ /**
+  * A set of static methods working on GeoAPI referencing objects.
+  * Some of those methods may be useful, but not really rigorous.
+  * This is why they do not appear in the public packages.
+  *
+  * <p><strong>Do not rely on this API!</strong> It may change in incompatible way in any future release.</p>
+  *
+  * @author  Martin Desruisseaux (IRD, Geomatys)
+  */
+ public final class ReferencingUtilities extends Static {
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private ReferencingUtilities() {
+     }
+ 
+     /**
+      * Returns the longitude value relative to the Greenwich Meridian, expressed in the specified units.
+      * This method provides the same functionality as {@link DefaultPrimeMeridian#getGreenwichLongitude(Unit)},
+      * but on arbitrary implementation.
+      *
+      * @param  primeMeridian  the prime meridian from which to get the Greenwich longitude, or {@code null}.
+      * @param  unit           the unit for the prime meridian to return.
+      * @return the prime meridian in the given units, or {@code 0} if the given prime meridian was null.
+      *
+      * @see DefaultPrimeMeridian#getGreenwichLongitude(Unit)
+      * @see org.apache.sis.referencing.CRS#getGreenwichLongitude(GeodeticCRS)
+      */
+     public static double getGreenwichLongitude(final PrimeMeridian primeMeridian, final Unit<Angle> unit) {
+         if (primeMeridian == null) {
+             return 0;
+         } else if (primeMeridian instanceof DefaultPrimeMeridian) {         // Maybe the user overrode some methods.
+             return ((DefaultPrimeMeridian) primeMeridian).getGreenwichLongitude(unit);
+         } else {
+             return primeMeridian.getAngularUnit().getConverterTo(unit).convert(primeMeridian.getGreenwichLongitude());
+         }
+     }
+ 
+     /**
+      * Returns the unit used for all axes in the given coordinate system.
+      * If not all axes use the same unit, then this method returns {@code null}.
+      *
+      * <p>This method is used either when the coordinate system is expected to contain exactly one axis,
+      * or for operations that support only one units for all axes, for example Well Know Text version 1
+      * (WKT 1) formatting.</p>
+      *
+      * @param  cs  the coordinate system for which to get the unit, or {@code null}.
+      * @return the unit for all axis in the given coordinate system, or {@code null}.
+      *
+      * @see org.apache.sis.referencing.privy.AxisDirections#getAngularUnit(CoordinateSystem, Unit)
+      */
+     public static Unit<?> getUnit(final CoordinateSystem cs) {
+         Unit<?> unit = null;
+         if (cs != null) {
+             for (int i=cs.getDimension(); --i>=0;) {
+                 final CoordinateSystemAxis axis = cs.getAxis(i);
+                 if (axis != null) {                                         // Paranoiac check.
+                     final Unit<?> candidate = axis.getUnit();
+                     if (candidate != null) {
+                         if (unit == null) {
+                             unit = candidate;
+                         } else if (!unit.equals(candidate)) {
+                             return null;
+                         }
+                     }
+                 }
+             }
+         }
+         return unit;
+     }
+ 
+     /**
+      * Returns the unit used for all axes in the given coordinate reference system.
+      * If not all axes use the same unit, then this method returns {@code null}.
+      *
+      * <p>This method is used either when the CRS is expected to contain exactly one axis,
+      * or for operations that support only one units for all axes.</p>
+      *
+      * @param  crs  the coordinate reference system for which to get the unit, or {@code null}.
+      * @return the unit for all axis in the given coordinate system, or {@code null}.
+      */
+     public static Unit<?> getUnit(final CoordinateReferenceSystem crs) {
+         return (crs != null) ? getUnit(crs.getCoordinateSystem()) : null;
+     }
+ 
+     /**
+      * Returns the number of dimensions of the given CRS, or 0 if {@code null}.
+      *
+      * @param  crs  the CRS from which to get the number of dimensions, or {@code null}.
+      * @return the number of dimensions, or 0 if the given CRS or its coordinate system is null.
+      */
+     public static int getDimension(final CoordinateReferenceSystem crs) {
+         if (crs != null) {
+             final CoordinateSystem cs = crs.getCoordinateSystem();
+             if (cs != null) {                                               // Paranoiac check.
+                 return cs.getDimension();
+             }
+         }
+         return 0;
+     }
+ 
+     /**
+      * Returns the GeoAPI interface implemented by the given object, or the implementation class
+      * if the interface is unknown. This method can be used when the base type (CRS, CS, Datum…)
+      * is unknown, for example when preparing an error message. If the base type is known, then
+      * the method expecting a {@code baseType} argument should be preferred.
+      *
+      * @param  object    the object for which to get the GeoAPI interface, or {@code null}.
+      * @return GeoAPI interface or implementation class of the given object, or {@code null} if the given object is null.
+      */
+     @SuppressWarnings("unchecked")
+     public static Class<?> getInterface(final Object object) {
+         if (object instanceof AbstractIdentifiedObject) {
+             return ((AbstractIdentifiedObject) object).getInterface();
+         } else {
+             return getInterface(IdentifiedObject.class, (Class) Classes.getClass(object));
+         }
+     }
+ 
+     /**
+      * Returns the GeoAPI interface implemented by the given object, or the implementation class
+      * if the interface is unknown.
+      *
+      * @param  <T>       compile-time value of {@code baseType}.
+      * @param  baseType  parent interface of the desired type.
+      * @param  object    the object for which to get the GeoAPI interface, or {@code null}.
+      * @return GeoAPI interface or implementation class of the given object, or {@code null} if the given object is null.
+      */
+     public static <T extends IdentifiedObject> Class<? extends T> getInterface(final Class<T> baseType, final T object) {
+         if (object instanceof AbstractIdentifiedObject) {
+             return ((AbstractIdentifiedObject) object).getInterface().asSubclass(baseType);
+         } else {
+             return getInterface(baseType, Classes.getClass(object));
+         }
+     }
+ 
+     /**
+      * Returns the GeoAPI interface implemented by the given class, or the class itself if the interface is unknown.
+      *
+      * @param  <T>       compile-time value of {@code baseType}.
+      * @param  baseType  parent interface of the desired type.
+      * @param  type      type of object for which to get the GeoAPI interface, or {@code null}.
+      * @return GeoAPI interface or implementation class, or {@code null} if the given type is null.
+      */
+     public static <T extends IdentifiedObject> Class<? extends T> getInterface(final Class<T> baseType, final Class<? extends T> type) {
+         final Class<? extends T>[] types = Classes.getLeafInterfaces(type, baseType);
+         return (types.length != 0) ? types[0] : type;
+     }
+ 
+     /**
+      * Copies all {@link SingleCRS} components from the given source to the given collection.
+      * For each {@link CompoundCRS} element found in the iteration, this method replaces the
+      * {@code CompoundCRS} by its {@linkplain CompoundCRS#getComponents() components}, which
+      * may themselves have other {@code CompoundCRS}. Those replacements are performed recursively
+      * until we obtain a flat view of CRS components.
+      *
+      * @param  source  the collection of single or compound CRS.
+      * @param  addTo   where to add the single CRS in order to obtain a flat view of {@code source}.
+      * @return {@code true} if this method found only single CRS in {@code source}, in which case {@code addTo}
+      *         got the same content (assuming that {@code addTo} was empty prior this method call).
+      * @throws NoSuchElementException if a CRS component is missing.
+      * @throws ClassCastException if a CRS is neither a {@link SingleCRS} or a {@link CompoundCRS}.
+      *
+      * @see org.apache.sis.referencing.CRS#getSingleComponents(CoordinateReferenceSystem)
+      */
+     public static boolean getSingleComponents(final Iterable<? extends CoordinateReferenceSystem> source,
+             final Collection<? super SingleCRS> addTo) throws ClassCastException
+     {
+         boolean sameContent = true;
+         for (final CoordinateReferenceSystem candidate : source) {
+             if (candidate instanceof CompoundCRS) {
+                 getSingleComponents(((CompoundCRS) candidate).getComponents(), addTo);
+                 sameContent = false;
+             } else if (candidate instanceof SingleCRS) {
+                 addTo.add((SingleCRS) candidate);
+             } else {
+                 /*
+                  * Illegal class. Try to provide a better error message, in particular when the CRS component
+                  * is nil because it is an unresolved xlink in a GML document. Nil objects are proxies, which
+                  * have hard to understand class names.
+                  */
+                 final String message;
+                 if (candidate instanceof NilObject) {
+                     message = Errors.format(Errors.Keys.NilObject_1, Identifiers.getNilReason((NilObject) candidate));
+                     throw new NoSuchElementException(message);
+                 } else {
+                     message = Errors.format(Errors.Keys.NestedElementNotAllowed_1, getInterface(candidate));
+                     throw new ClassCastException(message);
+                 }
+             }
+         }
+         return sameContent;
+     }
+ 
+     /**
+      * Returns {@code true} if the type of the given datum is ellipsoidal. A vertical datum is not allowed
+      * to be ellipsoidal according ISO 19111, but Apache SIS relaxes this restriction in some limited cases,
+      * for example when parsing a string in the legacy WKT 1 format. Apache SIS should not expose those
+      * vertical heights as much as possible, and instead try to combine them with three-dimensional
+      * geographic or projected CRS as soon as it can.
+      *
+      * @param  datum  the datum to test, or {@code null} if none.
+      * @return {@code true} if the given datum is non null and of ellipsoidal type.
+      *
+      * @see org.apache.sis.referencing.internal.VerticalDatumTypes#ELLIPSOIDAL
+      */
+     public static boolean isEllipsoidalHeight(final VerticalDatum datum) {
+         if (datum != null) {
+             final VerticalDatumType type = datum.getVerticalDatumType();
+             if (type != null) {
+                 return "ELLIPSOIDAL".equalsIgnoreCase(type.name());
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns the ellipsoid used by the given coordinate reference system, or {@code null} if none.
+      * More specifically:
+      *
+      * <ul>
+      *   <li>If the given CRS is an instance of {@link SingleCRS} and its datum is a {@link GeodeticDatum},
+      *       then this method returns the datum ellipsoid.</li>
+      *   <li>Otherwise if the given CRS is an instance of {@link CompoundCRS}, then this method
+      *       invokes itself recursively for each component until a geodetic datum is found.</li>
+      *   <li>Otherwise this method returns {@code null}.</li>
+      * </ul>
+      *
+      * Note that this method does not check if there is more than one ellipsoid (it should never be the case).
+      *
+      * @param  crs  the coordinate reference system for which to get the ellipsoid.
+      * @return the ellipsoid, or {@code null} if none.
+      */
+     public static Ellipsoid getEllipsoid(final CoordinateReferenceSystem crs) {
+         if (crs != null) {
+             if (crs instanceof SingleCRS) {
+                 final Datum datum = ((SingleCRS) crs).getDatum();
+                 if (datum instanceof GeodeticDatum) {
+                     final Ellipsoid e = ((GeodeticDatum) datum).getEllipsoid();
+                     if (e != null) return e;
+                 }
+             }
+             if (crs instanceof CompoundCRS) {
+                 for (final CoordinateReferenceSystem c : ((CompoundCRS) crs).getComponents()) {
+                     final Ellipsoid e = getEllipsoid(c);
+                     if (e != null) return e;
+                 }
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns the ellipsoid used by the specified coordinate reference system, provided that the two first dimensions
+      * use an instance of {@link GeographicCRS}. Otherwise (i.e. if the two first dimensions are not geographic),
+      * returns {@code null}.
+      *
+      * <p>This method excludes geocentric CRS on intent. Some callers needs this exclusion as a way to identify
+      * which CRS in a Geographic/Geocentric conversion is the geographic one. Another point of view is to said
+      * that if this method returns a non-null value, then the coordinates are expected to be either two-dimensional
+      * or three-dimensional with an ellipsoidal height.</p>
+      *
+      * @param  crs  the coordinate reference system for which to get the ellipsoid.
+      * @return the ellipsoid in the given CRS, or {@code null} if none.
+      */
+     public static Ellipsoid getEllipsoidOfGeographicCRS(CoordinateReferenceSystem crs) {
+         while (!(crs instanceof GeodeticCRS)) {
+             if (crs instanceof CompoundCRS) {
+                 crs = ((CompoundCRS) crs).getComponents().get(0);
+             } else {
+                 return null;
+             }
+         }
+         /*
+          * 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
+          * interface is GeoAPI-specific, so a CRS may be OGC-compliant without implementing that interface.
+          */
+         if (crs.getCoordinateSystem() instanceof EllipsoidalCS) {
+             return ((GeodeticCRS) crs).getDatum().getEllipsoid();
+         } else {
+             return null;    // Geocentric CRS.
+         }
+     }
+ 
+     /**
+      * Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>) axis in the specified
+      * order and in decimal degrees. If no such CRS can be obtained or created, returns {@code null}.
+      *
+      * <p>This method does not set the prime meridian to Greenwich.
+      * Meridian rotation, if needed, shall be performed by the caller.</p>
+      *
+      * @param  crs      a source CRS, or {@code null}.
+      * @param  latlon   {@code true} for (latitude, longitude) axis order, or {@code false} for (longitude, latitude).
+      * @param  allow3D  whether this method is allowed to return three-dimensional CRS (with ellipsoidal height).
+      * @return a two-dimensional geographic CRS with standard axes, or {@code null} if none.
+      */
+     public static GeographicCRS toNormalizedGeographicCRS(CoordinateReferenceSystem crs, final boolean latlon, final boolean allow3D) {
+         /*
+          * ProjectedCRS instances always have a GeographicCRS as their base.
+          * More generally, derived CRS are always derived from a base, which
+          * is often (but not necessarily) geographic.
+          */
+         while (crs instanceof GeneralDerivedCRS) {
+             crs = ((GeneralDerivedCRS) crs).getBaseCRS();
+         }
+         if (crs instanceof GeodeticCRS) {
+             /*
+              * At this point we usually have a GeographicCRS, but it could also be a GeocentricCRS.
+              * If we can let `forConvention` do its job, do that first since it may return a cached
+              * instance. If the CRS is a `GeographicCRS` but not a `DefaultGeographicCRS`, create a
+              * CRS in this code instead of invoking `DefaultGeographicCRS.castOrCopy(…)` in order
+              * to create only one CRS instead of two.
+              */
+             final CoordinateSystem cs = crs.getCoordinateSystem();
+             if (!latlon && crs instanceof DefaultGeographicCRS && (allow3D || cs.getDimension() == 2)) {
+                 return ((DefaultGeographicCRS) crs).forConvention(AxesConvention.NORMALIZED);
+             }
+             /*
+              * Get a normalized coordinate system with the number of dimensions authorized by the
+              * `allow3D` argument. We do not check if we can invoke `cs.forConvention(…)` because
+              * it is unlikely that `cs` will be an instance of `DefaultEllipsoidalCS` is `crs` is
+              * not a `DefaultGeographicCRS`. The code below has more chances to use cached instance.
+              */
+             EllipsoidalCS normalizedCS;
+             if (allow3D && cs.getDimension() >= 3) {
+                 normalizedCS = CommonCRS.WGS84.geographic3D().getCoordinateSystem();
+                 if (!latlon) {
+                     normalizedCS = DefaultEllipsoidalCS.castOrCopy(normalizedCS).forConvention(AxesConvention.NORMALIZED);
+                 }
+             } else if (latlon) {
+                 normalizedCS = CommonCRS.WGS84.geographic().getCoordinateSystem();
+             } else {
+                 normalizedCS = CommonCRS.defaultGeographic().getCoordinateSystem();
+             }
+             if (crs instanceof GeographicCRS && Utilities.equalsIgnoreMetadata(normalizedCS, cs)) {
+                 return (GeographicCRS) crs;
+             }
+             return new DefaultGeographicCRS(
+                     Map.of(DefaultGeographicCRS.NAME_KEY, NilReferencingObject.UNNAMED),
+                     ((GeodeticCRS) crs).getDatum(), normalizedCS);
+         }
+         if (crs instanceof CompoundCRS) {
+             for (final CoordinateReferenceSystem e : ((CompoundCRS) crs).getComponents()) {
+                 final GeographicCRS candidate = toNormalizedGeographicCRS(e, latlon, allow3D);
+                 if (candidate != null) {
+                     return candidate;
+                 }
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns {@code true} if the given coordinate system has at least 2 dimensions and
+      * the 2 first axis have (North, East) directions. This method is used for assertions.
+      *
+      * @param  cs  the coordinate system to verify.
+      * @return whether the coordinate system starts with (North, East) direction.
+      */
+     public static boolean startsWithNorthEast(final CoordinateSystem cs) {
+         final int dimension = cs.getDimension();
+         return (dimension >= 2)
+                 && cs.getAxis(0).getDirection() == AxisDirection.NORTH
+                 && cs.getAxis(1).getDirection() == AxisDirection.EAST;
+     }
+ 
+     /**
+      * Returns the properties (scope, domain of validity) except the identifiers and the EPSG namespace.
+      * The identifiers are removed because a modified CRS is no longer conform to the authoritative definition.
+      * If the name contains a namespace (e.g. "EPSG"), this method removes that namespace for the same reason.
+      * For example, "EPSG:WGS 84" will become simply "WGS 84".
+      *
+      * @param  object     the identified object for which to get properties map.
+      * @param  overwrite  properties overwriting the inherited ones, or {@code null} if none.
+      * @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 Identifier name = object.getName();
++        final ReferenceIdentifier name = object.getName();
+         final boolean keepName = name.getCodeSpace() == null && name.getAuthority() == null;
+         if (keepName && overwrite == null) {
+             return properties;
+         }
+         final var copy = new HashMap<String,Object>(properties);
+         if (!keepName) {
+             copy.put(IdentifiedObject.NAME_KEY, new NamedIdentifier(null, name.getCode()));
+         }
+         if (overwrite != null) {
+             copy.putAll(overwrite);
+         }
+         return copy;
+     }
+ 
+     /**
+      * Returns the properties of the given object but potentially with a modified name.
+      * Current implement truncates the name at the first non-white character which is not
+      * a valid Unicode identifier part, with the following exception:
+      *
+      * <ul>
+      *   <li>If the character is {@code '('} and the content until the closing {@code ')'} is a valid
+      *       Unicode identifier, then that part is included. The intent is to keep the prime meridian
+      *       name in names like <q>NTF (Paris)</q>.</li>
+      * </ul>
+      *
+      * <p>Examples:</p>
+      * <ul>
+      *   <li><q>NTF (Paris)</q> is left unchanged.</li>
+      *   <li><q>WGS 84 (3D)</q> is truncated as <q>WGS 84</q>.</li>
+      *   <li><q>Ellipsoidal 2D CS. Axes: latitude, longitude. Orientations: north, east. UoM: degree</q>
+      *       is truncated as <q>Ellipsoidal 2D CS</q>.</li>
+      * </ul>
+      *
+      * @param  object    the identified object to view as a properties map.
+      * @return a view of the identified object properties.
+      *
+      * @see IdentifiedObjects#getProperties(IdentifiedObject, String...)
+      */
+     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();
+             if (name != null) {
+                 for (int i=0; i < name.length();) {
+                     final int c = name.codePointAt(i);
+                     if (!Character.isUnicodeIdentifierPart(c) && !Character.isSpaceChar(c)) {
+                         if (c == '(') {
+                             final int endAt = name.indexOf(')', i);
+                             if (endAt >= 0) {
+                                 final String extra = name.substring(i+1, endAt);
+                                 if (CharSequences.isUnicodeIdentifier(extra)) {
+                                     i += extra.length() + 2;
+                                 }
+                             }
+                         }
+                         name = CharSequences.trimWhitespaces(name, 0, i).toString();
+                         if (!name.isEmpty()) {
+                             final Map<String,Object> copy = new HashMap<>(properties);
+                             copy.put(IdentifiedObject.NAME_KEY, name);
+                             return copy;
+                         }
+                     }
+                     i += Character.charCount(c);
+                 }
+             }
+         }
+         return properties;
+     }
+ 
+     /**
+      * Returns the XML property name of the given interface.
+      *
+      * For {@link CoordinateSystem} base type, the returned value shall be one of
+      * {@code affineCS}, {@code cartesianCS}, {@code cylindricalCS}, {@code ellipsoidalCS}, {@code linearCS},
+      * {@code parametricCS}, {@code polarCS}, {@code sphericalCS}, {@code timeCS} or {@code verticalCS}.
+      *
+      * @param  base  the abstract base interface.
+      * @param  type  the interface or classes for which to get the XML property name.
+      * @return the XML property name for the given class or interface, or {@code null} if none.
+      *
+      * @see WKTUtilities#toType(Class, Class)
+      */
+     public static StringBuilder toPropertyName(final Class<?> base, final Class<?> type) {
+         final UML uml = type.getAnnotation(UML.class);
+         if (uml != null) {
+             final Specification spec = uml.specification();
 -            if (spec == Specification.ISO_19111 || spec == Specification.ISO_19111_2) {
++            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);
+                 if (buffer.length() != 0) {
+                     buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
+                     return buffer;
+                 }
+             }
+         }
+         for (final Class<?> c : type.getInterfaces()) {
+             if (base.isAssignableFrom(c)) {
+                 final StringBuilder name = toPropertyName(base, c);
+                 if (name != null) {
+                     return name;
+                 }
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns the given factory instance if non-null, or a default instance otherwise.
+      *
+      * @param  factory  the factory, which may be {@code null}.
+      * @return the instance to use.
+      */
+     public static MathTransformFactory nonNull(MathTransformFactory factory) {
+         if (factory == null) {
+             factory = DefaultMathTransformFactory.provider().caching(false);
+         }
+         return factory;
+     }
+ 
+     /**
+      * Creates a context with source and target ellipsoids and coordinate systems inferred from the given CRS.
+      * The ellipsoids will be non-null only if the given CRS is geodetic (geographic or geocentric).
+      *
+      * @param  sourceCRS  the CRS from which to get the source coordinate system and ellipsoid, or {@code null}.
+      * @param  targetCRS  the CRS from which to get the target coordinate system and ellipsoid, or {@code null}.
+      * @return the context to provides to math transform factory.
+      */
+     public static Context createTransformContext(final CoordinateReferenceSystem sourceCRS,
+                                                  final CoordinateReferenceSystem targetCRS)
+     {
+         final Context context = new Context();
+         if (sourceCRS instanceof GeodeticCRS) {
+             context.setSource((GeodeticCRS) sourceCRS);
+         } else if (sourceCRS != null) {
+             context.setSource(sourceCRS.getCoordinateSystem());
+         }
+         if (targetCRS instanceof GeodeticCRS) {
+             context.setTarget((GeodeticCRS) targetCRS);
+         } else if (targetCRS != null) {
+             context.setTarget(targetCRS.getCoordinateSystem());
+         }
+         return context;
+     }
+ 
+     /**
+      * Substitute for the deprecated {@link MathTransformFactory#createBaseToDerived createBaseToDerived(…)} method.
+      * This substitute uses the full {@code targetCRS} instead of only the coordinate system of the target.
+      * This is needed for setting the {@code "tgt_semi_minor"} and {@code "tgt_semi_major"} parameters of
+      * Molodensky transformation for example.
+      *
+      * @param  factory     the factory to use for creating the transform.
+      * @param  sourceCRS   the source (base) coordinate reference system.
+      * @param  parameters  the parameter values for the transform.
+      * @param  targetCRS   the target (derived) coordinate system.
+      * @return the parameterized transform from {@code sourceCRS} to {@code targetCRS},
+      *         including unit conversions and axis swapping.
+      * @throws FactoryException if the object creation failed. This exception is thrown
+      *         if some required parameter has not been supplied, or has illegal value.
+      *
+      * @see <a href="https://issues.apache.org/jira/browse/SIS-512">SIS-512 on issues tracker</a>
+      */
+     public static MathTransform createBaseToDerived(final MathTransformFactory factory,
+             final CoordinateReferenceSystem sourceCRS, final ParameterValueGroup parameters,
+             final CoordinateReferenceSystem targetCRS) throws FactoryException
+     {
+         if (factory instanceof DefaultMathTransformFactory) {
+             return ((DefaultMathTransformFactory) factory).createParameterizedTransform(
+                     parameters, createTransformContext(sourceCRS, targetCRS));
+         } else {
+             // Fallback for non-SIS implementations. Work for map projections but not for Molodensky.
+             return factory.createBaseToDerived(sourceCRS, parameters, targetCRS.getCoordinateSystem());
+         }
+     }
+ 
+     /**
+      * Returns the mapping between parameter identifiers and parameter names as defined by the given authority.
+      * This method assumes that the identifiers of all parameters defined by that authority are numeric.
+      * Examples of authorities defining numeric parameters are EPSG and GeoTIFF.
+      *
+      * <p>The map returned by this method is modifiable. Callers are free to add or remove entries.</p>
+      *
+      * @param  parameters  the parameters for which to get a mapping from identifiers to names.
+      * @param  authority   the authority defining the parameters.
+      * @return mapping from parameter identifiers to parameter names defined by the given authority.
+      * @throws NumberFormatException if a parameter identifier of the given authority is not numeric.
+      * @throws IllegalArgumentException if the same identifier is used for two or more parameters.
+      */
+     public static Map<Integer,String> identifierToName(final ParameterDescriptorGroup parameters, final Citation authority) {
+         final Map<Integer,String> mapping = new HashMap<>();
+         for (final GeneralParameterDescriptor descriptor : parameters.descriptors()) {
+             final Identifier id = IdentifiedObjects.getIdentifier(descriptor, authority);
+             if (id != null) {
+                 String name = IdentifiedObjects.getName(descriptor, authority);
+                 if (name == null) {
+                     name = IdentifiedObjects.getName(descriptor, null);
+                 }
+                 if (mapping.put(Integer.valueOf(id.getCode()), name) != null) {
+                     throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedIdentifier_1, id));
+                 }
+             }
+         }
+         return mapping;
+     }
+ 
+     /**
+      * Returns short names for all axes of the given CRS. This method uses short names like "Latitude" or "Height",
+      * even if the full ISO 19111 names are "Geodetic latitude" or "Ellipsoidal height". This is suitable as header
+      * for columns in a table. This method does not include abbreviation or units in the returned names.
+      *
+      * @param  resources  the resources from which to get "latitude" and "longitude" localized labels.
+      * @param  crs        the coordinate reference system from which to get axis names.
+      * @return axis names, localized if possible.
+      */
+     public static String[] getShortAxisNames(final Vocabulary resources, final CoordinateReferenceSystem crs) {
+         final boolean isGeographic = (crs instanceof GeographicCRS);
+         final boolean isProjected  = (crs instanceof ProjectedCRS);
+         final CoordinateSystem cs = crs.getCoordinateSystem();
+         final String[] names = new String[cs.getDimension()];
+         for (int i=0; i<names.length; i++) {
+             short key = 0;
+             final CoordinateSystemAxis axis = cs.getAxis(i);
+             final AxisDirection direction = axis.getDirection();
+             if (AxisDirections.isCardinal(direction)) {
+                 final boolean isMeridional = (direction == AxisDirection.NORTH) || (direction == AxisDirection.SOUTH);
+                 if (isGeographic) {
+                     key = isMeridional ? Vocabulary.Keys.Latitude : Vocabulary.Keys.Longitude;
+                 } else if (isProjected) {
+                     // We could add "Easting" / "Northing" here for ProjectedCRS in a future version.
+                 }
+             } else if (direction == AxisDirection.UP) {
+                 if (isGeographic | isProjected) {
+                     key = Vocabulary.Keys.Height;
+                 }
+             }
+             names[i] = (key != 0) ? resources.getString(key) : axis.getName().getCode();
+         }
+         return names;
+     }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTUtilities.java
index 0000000000,0ca7754a66..6f0ad35c47
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTUtilities.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/WKTUtilities.java
@@@ -1,0 -1,548 +1,548 @@@
+ /*
+  * 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.referencing.privy;
+ 
+ import java.lang.reflect.Array;
+ import java.util.function.Function;
+ import java.util.logging.Logger;
+ import javax.measure.Unit;
+ import javax.measure.Quantity;
+ import javax.measure.quantity.Angle;
+ import org.opengis.parameter.ParameterValue;
+ import org.opengis.parameter.ParameterValueGroup;
+ import org.opengis.parameter.GeneralParameterValue;
+ import org.opengis.parameter.GeneralParameterDescriptor;
+ import org.opengis.referencing.IdentifiedObject;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.referencing.cs.CoordinateSystem;
+ import org.opengis.referencing.cs.CoordinateSystemAxis;
+ import org.opengis.referencing.datum.Datum;
+ import org.opengis.referencing.datum.GeodeticDatum;
+ import org.opengis.referencing.datum.PrimeMeridian;
+ import org.opengis.referencing.datum.Ellipsoid;
+ import org.opengis.referencing.operation.Matrix;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.apache.sis.referencing.IdentifiedObjects;
+ import org.apache.sis.referencing.crs.AbstractCRS;
+ import org.apache.sis.referencing.cs.AbstractCS;
+ import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
+ import org.apache.sis.referencing.datum.AbstractDatum;
+ import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
+ import org.apache.sis.referencing.datum.DefaultPrimeMeridian;
+ import org.apache.sis.referencing.datum.DefaultEllipsoid;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.referencing.operation.provider.Affine;
+ import org.apache.sis.system.Loggers;
+ import org.apache.sis.parameter.DefaultParameterValue;
+ import org.apache.sis.parameter.Parameterized;
+ import org.apache.sis.io.wkt.ElementKind;
+ import org.apache.sis.io.wkt.FormattableObject;
+ import org.apache.sis.io.wkt.Formatter;
+ import org.apache.sis.measure.Units;
+ import org.apache.sis.util.Static;
+ import org.apache.sis.util.CharSequences;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.privy.Numerics;
+ import org.apache.sis.math.DecimalFunctions;
+ import org.apache.sis.math.Statistics;
+ import org.apache.sis.math.Vector;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
++// Specific to the main branch:
++import org.opengis.referencing.ReferenceIdentifier;
+ 
+ 
+ /**
+  * Utility methods for referencing WKT formatting.
+  *
+  * This class provides a set of {@code toFormattable(…)} for various {@link IdentifiedObject} subtypes.
+  * It is important to <strong>not</strong> provide a generic {@code toFormattable(IdentifiedObject)}
+  * method, because the user may choose to implement more than one GeoAPI interface for the same object.
+  * We need to be specific in order to select the right "aspect" of the given object.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class WKTUtilities extends Static {
+     /**
+      * The logger for Well Known Text operations.
+      */
+     public static final Logger LOGGER = Logger.getLogger(Loggers.WKT);
+ 
+     /**
+      * Do not allow instantiation of this class.
+      */
+     private WKTUtilities() {
+     }
+ 
+     /**
+      * Returns the WKT type of the given interface.
+      *
+      * For {@link CoordinateSystem} base type, the returned value shall be one of
+      * {@code affine}, {@code Cartesian}, {@code cylindrical}, {@code ellipsoidal}, {@code linear},
+      * {@code parametric}, {@code polar}, {@code spherical}, {@code temporal} or {@code vertical}.
+      *
+      * @param  base  the abstract base interface.
+      * @param  type  the interface or classes for which to get the WKT type.
+      * @return the WKT type for the given class or interface, or {@code null} if none.
+      *
+      * @see ReferencingUtilities#toPropertyName(Class, Class)
+      */
+     public static String toType(final Class<?> base, final Class<?> type) {
+         if (type != base) {
+             final StringBuilder name = ReferencingUtilities.toPropertyName(base, type);
+             if (name != null) {
+                 int end = name.length() - 2;
+                 if (CharSequences.regionMatches(name, end, "CS")) {
+                     name.setLength(end);
+                     if ("time".contentEquals(name)) {
+                         return "temporal";
+                     }
+                     if (CharSequences.regionMatches(name, 0, "cartesian")) {
+                         name.setCharAt(0, 'C');     // "Cartesian"
+                     }
+                     return name.toString();
+                 }
... 1967 lines suppressed ...