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

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

commit 4e2e756dc100c682eace3fdb75f0098655e6d439
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Aug 24 16:03:43 2025 +0200

    In `IdentifiedObjectFinder`, retrofit the `createFromIdentifiers` and 
`createFromNames` methods into `getCodeCandidates`.
    This is needed because the previous algorithm assumed that CRS names were 
unique, which is not true in EPSG version 12.
    Also need to make the search for EPSG codes tolerant to the difference 
between datum and datum ensemble.
---
 .../sis/metadata/sql/privy/SQLUtilities.java       |  61 ++--
 .../org/apache/sis/metadata/sql/privy/Syntax.java  |   2 +-
 .../sis/metadata/sql/privy/SQLUtilitiesTest.java   |   4 +-
 .../sis/openoffice/ReferencingFunctions.java       |   2 +-
 .../apache/sis/referencing/AuthorityFactories.java |   2 +-
 .../apache/sis/referencing/IdentifiedObjects.java  |   6 +-
 .../factory/ConcurrentAuthorityFactory.java        |   8 +-
 .../factory/IdentifiedObjectFinder.java            | 131 +++-----
 .../referencing/factory/sql/AuthorityCodes.java    | 154 ++++++---
 .../factory/sql/CloseableReference.java            |   6 +
 .../referencing/factory/sql/EPSGCodeFinder.java    | 347 ++++++++++++++++----
 .../referencing/factory/sql/EPSGDataAccess.java    | 355 ++++++++++-----------
 .../referencing/factory/sql/ObjectPertinence.java  |  11 +-
 .../sis/referencing/factory/sql/SQLTranslator.java |  19 +-
 .../sis/referencing/factory/sql/TableInfo.java     |  28 +-
 .../operation/CoordinateOperationRegistry.java     |   5 +-
 .../sis/referencing/AuthorityFactoriesTest.java    |   2 +-
 .../referencing/factory/sql/EPSGFactoryTest.java   |  19 +-
 .../sis/storage/sql/feature/InfoStatements.java    |   2 +-
 .../main/org/apache/sis/util/logging/Logging.java  |   2 +-
 .../sis/referencing/factory/sql/epsg/Prepare.sql   |   4 +-
 21 files changed, 702 insertions(+), 468 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
index c4b9881d55..38e6e1f423 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
@@ -137,58 +137,69 @@ public final class SQLUtilities extends Static {
     }
 
     /**
-     * Returns a string like the given string but with accented letters 
replaced by <abbr>ASCII</abbr>
-     * letters and all characters that are not letter or digit replaced by the 
wildcard % character.
+     * Returns a string like the given string but with accented letters 
replaced by any character ({@code '_'})
+     * and all characters that are not letter or digit replaced by the 
wildcard ({@code '%'}).
      *
-     * @param  text     the text to get as a SQL LIKE pattern.
-     * @param  toLower  whether to convert characters to lower case.
-     * @return the "LIKE" pattern for the given text.
+     * @param  text         the text to get as a SQL LIKE pattern.
+     * @param  toLowerCase  whether to convert characters to lower case.
+     * @param  escape       value of {@link 
DatabaseMetaData#getSearchStringEscape()}. May be null or empty.
+     * @return the {@code LIKE} pattern for the given text.
      */
-    public static String toLikePattern(final String text, final boolean 
toLower) {
+    public static String toLikePattern(final String text, final boolean 
toLowerCase, final String escape) {
         final var buffer = new StringBuilder(text.length());
-        toLikePattern(text, 0, text.length(), false, toLower, buffer);
+        toLikePattern(text, 0, text.length(), false, toLowerCase, escape, 
buffer);
         return buffer.toString();
     }
 
     /**
      * Returns a <abbr>SQL</abbr> LIKE pattern for the given text. The text is 
optionally returned in all lower cases
      * for allowing case-insensitive searches. Punctuations are replaced by 
any sequence of characters ({@code '%'})
-     * and non-ASCII letters or digits are replaced by any single character 
({@code '_'}). This method avoid to put
-     * a {@code '%'} symbol as the first character since it prevents some 
databases to use their index.
+     * and non-<abbr>ASCII</abbr> Latin letters are replaced by any single 
character ({@code '_'}).
+     * Ideograms (Japanese, Chinese, …) and hiragana (Japanese) are kept 
unchanged.
+     * This method avoids to put a {@code '%'} symbol as the first character
+     * because such character prevents some databases to use their index.
      *
-     * @param  text         the text to get as a SQL LIKE pattern.
-     * @param  i            index of the first character to use in the given 
{@code identifier}.
-     * @param  end          index after the last character to use in the given 
{@code identifier}.
+     * @param  text         the text to get as a <abbr>SQL</abbr> {@code LIKE} 
pattern.
+     * @param  textStart    index of the first character to use in the given 
{@code text}.
+     * @param  textEnd      index after the last character to use in the given 
{@code text}.
      * @param  allowSuffix  whether to append a final {@code '%'} wildcard at 
the end of the pattern.
-     * @param  toLower      whether to convert characters to lower case.
-     * @param  buffer       buffer where to append the SQL LIKE pattern.
+     * @param  toLowerCase  whether to convert characters to lower case.
+     * @param  escape       value of {@link 
DatabaseMetaData#getSearchStringEscape()}. May be null or empty.
+     * @param  buffer       buffer where to append the <abbr>SQL</abbr> {@code 
LIKE} pattern.
      */
-    public static void toLikePattern(final String text, int i, final int end,
-            final boolean allowSuffix, final boolean toLower, final 
StringBuilder buffer)
+    public static void toLikePattern(final String text, int textStart, final 
int textEnd, final boolean allowSuffix,
+                                     final boolean toLowerCase, final String 
escape, final StringBuilder buffer)
     {
-        final int bs = buffer.length();
-        while (i < end) {
-            final int c = text.codePointAt(i);
+        final int bufferStart = buffer.length();
+        while (textStart < textEnd) {
+            final int c = text.codePointAt(textStart);
             if (Character.isLetterOrDigit(c)) {
-                if (c < 128) {                      // Use only ASCII 
characters in the search.
-                    buffer.appendCodePoint(toLower ? Character.toLowerCase(c) 
: c);
+                // Ignore accented letters and Greek letters (before `U+0400`) 
in the search.
+                if (c < 0x80 || c >= 0x400) {
+                    buffer.appendCodePoint(toLowerCase ? 
Character.toLowerCase(c) : c);
                 } else {
                     appendIfNotRedundant(buffer, '_');
                 }
             } else {
                 final int length = buffer.length();
-                if (length == bs) {
-                    buffer.appendCodePoint(c != '%' ? c : '_');
+                if (length == bufferStart) {
+                    // Do not use wildcard in the first character.
+                    if (escape != null && (c == '%' || c == '_' || 
text.startsWith(escape, textStart))) {
+                        // Note: there will be bug if `escape` is a repetition 
of the same character.
+                        // But we assume that this corner case is too rare for 
being worth a check.
+                        buffer.append(escape);
+                    }
+                    buffer.appendCodePoint(c);
                 } else if (buffer.charAt(length - 1) != '%') {
                     buffer.append('%');
                 }
             }
-            i += Character.charCount(c);
+            textStart += Character.charCount(c);
         }
         if (allowSuffix) {
             appendIfNotRedundant(buffer, '%');
         }
-        for (i=bs; (i = buffer.indexOf("_%", i)) >= 0;) {
+        for (int i=bufferStart; (i = buffer.indexOf("_%", i)) >= 0;) {
             buffer.deleteCharAt(i);
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
index ae9281ee90..218e6b0d03 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
@@ -74,7 +74,7 @@ public class Syntax {
      * @see #escapeWildcards(String)
      * @see SQLBuilder#appendWildcardEscaped(String)
      */
-    final String wildcardEscape;
+    public final String wildcardEscape;
 
     /**
      * The default catalog of the connection, or {@code null} if none.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java
index 538b5d2144..57ddfc8a7e 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java
@@ -55,7 +55,7 @@ public final class SQLUtilitiesTest extends TestCase {
         assertEquals("WGS%84",                      toLikePattern(buffer, "WGS 
84"));
         assertEquals("A%text%with%random%symbols%", toLikePattern(buffer, "A 
text !* with_random:/symbols;+"));
         assertEquals("*%With%non%letter%start",     toLikePattern(buffer, 
"*_+%=With non-letter  start"));
-        assertEquals("_Special%case",               toLikePattern(buffer, 
"%Special_case"));
+        assertEquals("\\%Special%case",             toLikePattern(buffer, 
"%Special_case"));
     }
 
     /**
@@ -63,7 +63,7 @@ public final class SQLUtilitiesTest extends TestCase {
      */
     private static String toLikePattern(final StringBuilder buffer, final 
String identifier) {
         buffer.setLength(0);
-        SQLUtilities.toLikePattern(identifier, 0, identifier.length(), false, 
false, buffer);
+        SQLUtilities.toLikePattern(identifier, 0, identifier.length(), false, 
false, "\\", buffer);
         return buffer.toString();
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java
 
b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java
index 2fbfc7b4d3..6384701b86 100644
--- 
a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java
+++ 
b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java
@@ -183,7 +183,7 @@ public class ReferencingFunctions extends CalcAddins 
implements XReferencing {
                 return object.getName().getCode();
             }
             // In Apache SIS implementation, `getDescriptionText(…)` returns 
the identified object name.
-            name = 
CRS.getAuthorityFactory(null).getDescriptionText(IdentifiedObject.class, 
codeOrPath).orElse(null);
+            name = 
CRS.getAuthorityFactory(null).getDescriptionText(CoordinateReferenceSystem.class,
 codeOrPath).orElse(null);
         } catch (Exception exception) {
             return getLocalizedMessage(exception);
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
index 711f3dde50..c20937e0a6 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
@@ -290,7 +290,7 @@ final class AuthorityFactories<T extends AuthorityFactory> 
extends LazySet<T> {
             }
 
             /** Returns a set of authority codes, using the fallback if 
necessary. */
-            @Override protected Set<String> getCodeCandidates(final 
IdentifiedObject object) throws FactoryException {
+            @Override protected Iterable<String> getCodeCandidates(final 
IdentifiedObject object) throws FactoryException {
                 for (;;) try {      // Executed at most twice.
                     return super.getCodeCandidates(object);
                 } catch (UnavailableFactoryException e) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
index da6e9567a4..6ea0d83d3d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
@@ -686,8 +686,6 @@ public final class IdentifiedObjects extends Static {
      * <h4>Example 2: extend the search to deprecated definitions</h4>
      * By default, {@code lookup(…)} methods exclude deprecated objects from 
the search.
      * To search also among deprecated objects, one can use the following Java 
code:
-     * This example does not use the {@code findSingleton(…)} convenience 
method on the assumption
-     * that the search may find both deprecated and non-deprecated objects.
      *
      * {@snippet lang="java" :
      *     IdentifiedObjectFinder finder = IdentifiedObjects.newFinder(null);
@@ -706,9 +704,7 @@ public final class IdentifiedObjects extends Static {
      * @see 
org.apache.sis.referencing.factory.GeodeticAuthorityFactory#newIdentifiedObjectFinder()
      * @see IdentifiedObjectFinder#find(IdentifiedObject)
      */
-    public static IdentifiedObjectFinder newFinder(final String authority)
-            throws NoSuchAuthorityFactoryException, FactoryException
-    {
+    public static IdentifiedObjectFinder newFinder(final String authority) 
throws NoSuchAuthorityFactoryException, FactoryException {
         final GeodeticAuthorityFactory factory;
         if (authority == null) {
             factory = AuthorityFactories.ALL;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
index 5f8fd800ea..af6b17c7c0 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
@@ -48,6 +48,7 @@ import org.opengis.parameter.ParameterDescriptor;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.Printable;
+import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
@@ -626,11 +627,11 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
         /*
          * We must close the factories from outside the synchronized block.
          */
-        confirmClose(factories);
         try {
+            confirmClose(factories);
             close(factories);
         } catch (Exception exception) {
-            unexpectedException("closeExpired", exception);
+            unexpectedException("closeExpired", Exceptions.unwrap(exception));
         }
         /*
          * If the queue of Data Access Objects (DAO) become empty, this means 
that this `ConcurrentAuthorityFactory`
@@ -1901,7 +1902,7 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
          * object than the specified one. This method delegates to the data 
access object.
          */
         @Override
-        protected synchronized Set<String> getCodeCandidates(final 
IdentifiedObject object) throws FactoryException {
+        protected synchronized Iterable<String> getCodeCandidates(final 
IdentifiedObject object) throws FactoryException {
             try {
                 acquire();
                 return finder.getCodeCandidates(object);
@@ -2176,6 +2177,7 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
             confirmClose(factories);
             close(factories);                       // Must be invoked outside 
the synchronized block.
         } catch (Exception e) {
+            e = Exceptions.unwrap(e);
             if (e instanceof FactoryException) {
                 throw (FactoryException) e;
             } else {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
index 2af9d2d64d..a5dfbcd4a5 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
@@ -21,20 +21,24 @@ import java.util.LinkedHashSet;
 import java.util.Objects;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.metadata.Identifier;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.AuthorityFactory;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
+import org.opengis.referencing.datum.Datum;
 import org.apache.sis.referencing.AbstractIdentifiedObject;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.privy.Constants;
-import org.apache.sis.system.Semaphores;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.system.Semaphores;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.referencing.datum.DatumEnsemble;
 
 
 /**
@@ -57,7 +61,7 @@ import org.apache.sis.util.logging.Logging;
  * is thread-safe. If concurrent searches are desired, then a new instance 
should be created for each thread.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see GeodeticAuthorityFactory#newIdentifiedObjectFinder()
  * @see IdentifiedObjects#newFinder(String)
@@ -261,7 +265,19 @@ public class IdentifiedObjectFinder {
      * more reliable than user-specified objects.
      */
     private boolean match(final IdentifiedObject candidate, final 
IdentifiedObject object) {
-        return Utilities.deepEquals(candidate, object, ignoreAxes ? 
ComparisonMode.ALLOW_VARIANT : COMPARISON_MODE);
+        final ComparisonMode mode = ignoreAxes ? ComparisonMode.ALLOW_VARIANT 
: COMPARISON_MODE;
+        if (Utilities.deepEquals(candidate, object, mode)) {
+            return true;
+        }
+        if (Datum.class.isAssignableFrom(proxy.type)) {
+            if (candidate instanceof Datum && object instanceof 
DatumEnsemble<?>) {
+                return DatumOrEnsemble.isLegacyDatum((DatumEnsemble<?>) 
object, (Datum) candidate, mode);
+            }
+            if (candidate instanceof DatumEnsemble<?> && object instanceof 
Datum) {
+                return DatumOrEnsemble.isLegacyDatum((DatumEnsemble<?>) 
candidate, (Datum) object, mode);
+            }
+        }
+        return false;
     }
 
     /**
@@ -288,20 +304,8 @@ public class IdentifiedObjectFinder {
 
     /**
      * Lookups objects which are approximately equal to the specified object.
-     * The default implementation tries to instantiate some {@linkplain 
AbstractIdentifiedObject identified objects}
-     * from the authority factory specified at construction time, in the 
following order:
-     *
-     * <ul>
-     *   <li>If the specified object contains {@linkplain 
AbstractIdentifiedObject#getIdentifiers() identifiers}
-     *       associated to the same authority as the factory, then those 
identifiers are used for creating the
-     *       objects to be compared by calls to a {@code create<Type>(String)} 
method.</li>
-     *   <li>If the authority factory can create objects from their 
{@linkplain AbstractIdentifiedObject#getName() name}
-     *       in addition of identifiers, then the name and {@linkplain 
AbstractIdentifiedObject#getAlias() aliases} are
-     *       used for creating objects to be tested.</li>
-     *   <li>If a full scan of the dataset is allowed, then full {@linkplain 
#getCodeCandidates set of candidate codes}
-     *       is used for creating objects to be tested.</li>
-     * </ul>
-     *
+     * This method tries to instantiate objects identified by the {@linkplain 
#getCodeCandidates set of candidate codes}
+     * with the authority factory specified at construction time.
      * The created objects which are equal to the specified object in the
      * the sense of {@link ComparisonMode#APPROXIMATE} are returned.
      *
@@ -322,14 +326,6 @@ public class IdentifiedObjectFinder {
                      * trust the identifiers in the user object.
                      */
                     IdentifiedObject candidate = createFromIdentifiers(object);
-                    if (candidate == null) {
-                        /*
-                         * We are unable to find the object from its 
identifiers. Try a quick name lookup.
-                         * Some implementations like the one backed by the 
EPSG database are capable to find
-                         * an object from its name.
-                         */
-                        candidate = createFromNames(object);
-                    }
                     if (candidate != null) {
                         result = Set.of(candidate);
                     }
@@ -408,7 +404,6 @@ public class IdentifiedObjectFinder {
      * @throws FactoryException if an error occurred while creating an object.
      *
      * @see #createFromCodes(IdentifiedObject)
-     * @see #createFromNames(IdentifiedObject)
      */
     private IdentifiedObject createFromIdentifiers(final IdentifiedObject 
object) throws FactoryException {
         for (final Identifier id : object.getIdentifiers()) {
@@ -436,56 +431,6 @@ public class IdentifiedObjectFinder {
         return null;
     }
 
-    /**
-     * Creates an object equals (optionally ignoring metadata), to the 
specified object using only the
-     * {@linkplain AbstractIdentifiedObject#getName name} and {@linkplain 
AbstractIdentifiedObject#getAlias aliases}.
-     * If no such object is found, returns {@code null}.
-     *
-     * <p>This method may be used with some {@linkplain 
GeodeticAuthorityFactory authority factory}
-     * implementations like the one backed by the EPSG database, which are 
capable to find an object
-     * from its name when the identifier is unknown.</p>
-     *
-     * @param  object  the object looked up.
-     * @return the identified object, or {@code null} if not found.
-     * @throws FactoryException if an error occurred while creating an object.
-     *
-     * @see #createFromCodes(IdentifiedObject)
-     * @see #createFromIdentifiers(IdentifiedObject)
-     */
-    private IdentifiedObject createFromNames(final IdentifiedObject object) 
throws FactoryException {
-        String code = object.getName().getCode();
-        IdentifiedObject candidate;
-        try {
-            candidate = create(code);
-        } catch (FactoryException e) {
-            /*
-             * The identifier was not recognized. We will continue later with 
aliases.
-             * Note: we catch a more generic exception than 
NoSuchAuthorityCodeException because
-             *       this attempt may fail for various reasons (character 
string not supported
-             *       by the underlying database for primary key, duplicated 
name found, etc.).
-             */
-            exceptionOccurred(e);
-            candidate = null;
-        }
-        if (match(candidate, object)) {
-            return candidate;
-        }
-        for (final GenericName id : object.getAlias()) {
-            code = id.toString();
-            try {
-                candidate = create(code);
-            } catch (FactoryException e) {
-                // The name was not recognized. No problem, let's go on.
-                exceptionOccurred(e);
-                continue;
-            }
-            if (match(candidate, object)) {
-                return candidate;
-            }
-        }
-        return null;
-    }
-
     /**
      * Creates an object equals (optionally ignoring metadata), to the 
specified object.
      * This method scans the {@linkplain #getCodeCandidates(IdentifiedObject) 
authority codes},
@@ -495,20 +440,14 @@ public class IdentifiedObjectFinder {
      * <p>This method may be used in order to get a fully {@linkplain 
AbstractIdentifiedObject identified object}
      * from an object without {@linkplain 
AbstractIdentifiedObject#getIdentifiers() identifiers}.</p>
      *
-     * <p>Scanning the whole set of authority codes may be slow. Users should 
try
-     * <code>{@linkplain #createFromIdentifiers 
createFromIdentifiers}(object)</code> and/or
-     * <code>{@linkplain #createFromNames createFromNames}(object)</code> 
before to fallback
-     * on this method.</p>
-     *
      * @param  object  the object looked up.
      * @return the identified object, or {@code null} if not found.
      * @throws FactoryException if an error occurred while scanning through 
authority codes.
      *
      * @see #createFromIdentifiers(IdentifiedObject)
-     * @see #createFromNames(IdentifiedObject)
      */
     Set<IdentifiedObject> createFromCodes(final IdentifiedObject object) 
throws FactoryException {
-        final Set<IdentifiedObject> result = new LinkedHashSet<>();     // We 
need to preserve order.
+        final var result = new LinkedHashSet<IdentifiedObject>();     // We 
need to preserve order.
         final boolean finer = 
Semaphores.queryAndSet(Semaphores.FINER_OBJECT_CREATION_LOGS);
         try {
             for (final String code : getCodeCandidates(object)) {
@@ -557,15 +496,21 @@ public class IdentifiedObjectFinder {
 
     /**
      * Returns a set of authority codes that <em>may</em> identify the same 
object as the specified one.
-     * The returned set must contains <em>at least</em> the code of every 
objects that are
-     * {@link ComparisonMode#APPROXIMATE approximately equal} to the specified 
one.
-     * However, the set may conservatively contains the code for more objects 
if an exact search is too expensive.
+     * The elements may be determined from object identifiers, from object 
names, or from a more extensive search in the database.
+     * The effort in populating the returned set is specified by the 
{@linkplain #getSearchDomain() search domain}.
+     * The returned set should contain at least the codes of every objects in 
the search domain
+     * that are {@linkplain ComparisonMode#APPROXIMATE approximately equal} to 
the specified object.
+     * However, the set may conservatively contain the codes for more objects 
if an exact search is too expensive.
      *
      * <p>This method is invoked by the default {@link 
#find(IdentifiedObject)} method implementation.
-     * The caller iterates through the returned codes, instantiate the objects 
and compare them with
-     * the specified one in order to determine which codes are really 
applicable.
-     * The iteration stops as soon as a match is found (in other words, if 
more than one object is equal
-     * to the specified one, then the {@code find(…)} method selects the first 
one in iteration order).</p>
+     * The caller iterates through the returned codes, instantiates the 
objects and compares them with
+     * the specified object in order to determine which codes are really 
matching.
+     * The iteration order should be the preference order.</p>
+     *
+     * <h4>Exceptions during iteration</h4>
+     * An unchecked {@link BackingStoreException} may be thrown during the 
iteration if the implementation
+     * fetches the codes lazily (when first needed) from the authority 
factory, and that action failed.
+     * The exception cause is often the checked {@link FactoryException}.
      *
      * <h4>Default implementation</h4>
      * The default implementation returns the same set as
@@ -577,7 +522,7 @@ public class IdentifiedObjectFinder {
      * @return a set of code candidates.
      * @throws FactoryException if an error occurred while fetching the set of 
code candidates.
      */
-    protected Set<String> getCodeCandidates(final IdentifiedObject object) 
throws FactoryException {
+    protected Iterable<String> getCodeCandidates(final IdentifiedObject 
object) throws FactoryException {
         return 
factory.getAuthorityCodes(proxy.type.asSubclass(IdentifiedObject.class));
     }
 
@@ -691,7 +636,7 @@ public class IdentifiedObjectFinder {
          * The default method implementation delegates the work to the finder 
specified by {@link #delegate()}.
          */
         @Override
-        protected Set<String> getCodeCandidates(final IdentifiedObject object) 
throws FactoryException {
+        protected Iterable<String> getCodeCandidates(final IdentifiedObject 
object) throws FactoryException {
             @SuppressWarnings("LocalVariableHidesMemberVariable")
             final IdentifiedObjectFinder delegate = delegate();
             return (delegate != this) ? delegate.getCodeCandidates(object) : 
super.getCodeCandidates(object);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
index ab1f20c895..2b26f07d70 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
@@ -16,14 +16,15 @@
  */
 package org.apache.sis.referencing.factory.sql;
 
+import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.io.Serializable;
 import java.io.ObjectStreamException;
 import java.sql.ResultSet;
-import java.sql.Connection;
 import java.sql.SQLException;
 import java.sql.PreparedStatement;
 import java.sql.Statement;
+import org.apache.sis.metadata.sql.privy.SQLUtilities;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.IntegerList;
 import org.apache.sis.util.privy.AbstractMap;
@@ -32,7 +33,7 @@ import org.apache.sis.util.privy.Strings;
 
 /**
  * A map of <abbr>EPSG</abbr> authority codes as keys and object names as 
values.
- * This map requires a living connection to the EPSG database.
+ * This map requires a valid connection to the <abbr>EPSG</abbr> database.
  *
  * <h2>Serialization</h2>
  * Serialization of this class stores a copy of all authority codes.
@@ -63,7 +64,7 @@ final class AuthorityCodes extends AbstractMap<String,String> 
implements Seriali
     /**
      * Index in the {@link #sql} and {@link #statements} arrays.
      */
-    private static final int ALL = 0, ONE = 1;
+    private static final int ALL_CODES = 0, NAME_FOR_CODE = 1, CODES_FOR_NAME 
= 2;
 
     /**
      * The factory which is the owner of this map. One purpose of this field 
is to prevent
@@ -83,12 +84,14 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      * In this array:
      *
      * <ul>
-     *   <li>{@code sql[ALL]} is a statement for querying all codes.</li>
-     *   <li>{@code sql[ONE]} is a statement for querying a single code.
-     *       This statement is similar to {@code sql[ALL]} with the addition 
of a {@code WHERE} clause.</li>
+     *   <li>{@code sql[ALL_CODES]}      is a statement for querying all 
codes.</li>
+     *   <li>{@code sql[NAME_FOR_CODE]}  is a statement for querying the name 
associated to a single code.</li>
+     *   <li>{@code sql[CODES_FOR_NAME]} is a statement for querying the 
code(s) for an object of a given name.</li>
      * </ul>
+     *
+     * The array length may be only 1 instead of 3 if there is no 
<abbr>SQL</abbr> statement for fetching the name.
      */
-    private final transient String[] sql = new String[2];
+    private final transient String[] sql;
 
     /**
      * The JDBC statements for the SQL commands in the {@link #sql} array, 
created when first needed.
@@ -96,15 +99,15 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      * This array will also be stored in {@link CloseableReference} for 
closing the statements
      * when the garbage collector detected that {@code AuthorityCodes} is no 
longer in use.
      */
-    private final transient Statement[] statements = new Statement[2];
+    private final transient Statement[] statements;
 
     /**
-     * The result of {@code statements[ALL]}, created only if requested.
+     * The result of {@code statements[ALL_CODES]}, created only if requested.
      * The codes will be queried at most once and cached in the {@link #codes} 
list.
      *
      * <p>Note that if this result set is not closed explicitly, it will be 
closed implicitly when
-     * {@code statements[ALL]} will be closed. This is because JDBC 
specification said that closing
-     * a statement also close its result set.</p>
+     * {@code statements[ALL_CODES]} will be closed. This is because 
<abbr>JDBC</abbr> specification
+     * said that closing a statement also close its result set.</p>
      */
     private transient ResultSet results;
 
@@ -116,49 +119,60 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
     /**
      * Creates a new map of authority codes for the specified type.
      *
-     * @param  connection  the connection to the EPSG database.
-     * @param  table       the table to query.
-     * @param  type        the type to query.
-     * @param  factory     the factory originator.
+     * @param table    the table to query.
+     * @param type     the type to query.
+     * @param factory  the factory originator.
      */
-    AuthorityCodes(final Connection connection, final TableInfo table, final 
Class<?> type, final EPSGDataAccess factory)
-            throws SQLException
-    {
+    AuthorityCodes(final TableInfo table, final Class<?> type, final 
EPSGDataAccess factory) throws SQLException {
         this.factory = factory;
+        final int count = (table.nameColumn != null) ? 3 : 1;
+        sql = new String[count];
+        statements = new Statement[count];
         /*
          * Build the SQL query for fetching the codes of all object. It is of 
the form:
          *
-         *     SELECT code FROM table ORDER BY code;
+         *     SELECT code FROM table WHERE DEPRECATED=FALSE ORDER BY code;
          */
         final var buffer = new StringBuilder(100);
         final int columnNameStart = buffer.append("SELECT ").length();
         final int columnNameEnd = buffer.append(table.codeColumn).length();
-        buffer.append(" FROM ").append(table.table);
-        final Class<?> tableType = table.where(factory, type, buffer);
+        buffer.append(" FROM ").append(table.fromClause);
+        this.type = table.where(factory, type, buffer);
         final int conditionStart = buffer.length();
         if (table.showColumn != null) {
             buffer.append(table.showColumn).append("=TRUE AND ");
-            // Do not put spaces around "<>" - SQLTranslator searches for this 
exact match.
         }
         // Do not put spaces around "=" - SQLTranslator searches for this 
exact match.
-        buffer.append("DEPRECATED=FALSE ORDER BY ").append(table.codeColumn);
-        sql[ALL] = factory.translator.apply(buffer.toString());
+        sql[ALL_CODES] = buffer.append("DEPRECATED=FALSE ORDER BY 
").append(table.codeColumn).toString();
+        /*
+         * Build the SQL query for fetching the codes of object having a name 
matching a pattern.
+         * It is of the form:
+         *
+         *     SELECT code FROM table WHERE name LIKE ? AND DEPRECATED=FALSE 
ORDER BY code;
+         */
+        if (count > CODES_FOR_NAME) {
+            sql[CODES_FOR_NAME] = buffer.insert(conditionStart, 
table.nameColumn + " LIKE ? AND ").toString();
+            /*
+             * Workaround for Derby bug. See 
`SQLUtilities.filterFalsePositive(…)`.
+             */
+            String t = sql[CODES_FOR_NAME];
+            t = t.substring(0, columnNameEnd) + ", " + table.nameColumn + 
t.substring(columnNameEnd);
+            sql[CODES_FOR_NAME] = t;
+        }
         /*
          * Build the SQL query for fetching the name of a single object for a 
given code.
          * This query will also be used for testing object existence. It is of 
the form:
          *
          *     SELECT name FROM table WHERE code = ?
          */
-        buffer.setLength(conditionStart);
-        if (table.nameColumn != null) {
+        if (count > NAME_FOR_CODE) {
+            buffer.setLength(conditionStart);
             buffer.replace(columnNameStart, columnNameEnd, table.nameColumn);
+            sql[NAME_FOR_CODE] = buffer.append(table.codeColumn).append(" = 
?").toString();
+        }
+        for (int i=0; i<count; i++) {
+            sql[i] = factory.translator.apply(sql[i]);
         }
-        buffer.append(table.codeColumn).append(" = ?");
-        sql[ONE] = factory.translator.apply(buffer.toString());
-        /*
-         * Other information opportunistically computed from above search.
-         */
-        this.type = tableType;
     }
 
     /**
@@ -170,6 +184,64 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
         return new CloseableReference(this, factory, statements);
     }
 
+    /**
+     * Returns the prepared statement at the given index, creating it when 
first needed.
+     * This method must be invoked in a block synchronized on {@link #factory}.
+     */
+    private PreparedStatement prepareStatement(final int index) throws 
SQLException {
+        var statement = (PreparedStatement) statements[index];
+        if (statement == null) {
+            statements[index] = statement = 
factory.connection.prepareStatement(sql[index]);
+            sql[index] = null;    // Not needed anymore.
+        }
+        return statement;
+    }
+
+    /**
+     * Puts codes associated to the given name in the given collection.
+     *
+     * @param  pattern  the {@code LIKE} pattern of the name to search.
+     * @param  name     the original name (workaround for Derby bug).
+     * @param  addTo    the collection where to add the codes.
+     * @throws SQLException if an error occurred while querying the database.
+     */
+    final void findCodesFromName(final String pattern, final String name, 
final Collection<Integer> addTo) throws SQLException {
+        if (statements.length > CODES_FOR_NAME) {
+            synchronized (factory) {
+                final PreparedStatement statement = 
prepareStatement(CODES_FOR_NAME);
+                statement.setString(1, pattern);
+                try (ResultSet result = statement.executeQuery()) {
+                    while (result.next()) {
+                        final int code = result.getInt(1);
+                        if (!result.wasNull() && 
SQLUtilities.filterFalsePositive(name, result.getString(2))) {
+                            addTo.add(code);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Puts all codes in the given collection. This method is used only as a 
fallback when {@link EPSGCodeFinder}
+     * cannot get a list of authority codes in a more selective way, with some 
conditions on property values.
+     * This method should not be invoked for the most common objects such as 
<abbr>CRS</abbr> and datum.
+     *
+     * @param  addTo  the collection where to add all codes.
+     * @return whether the collection has changed as a result of this method 
call.
+     * @throws SQLException if an error occurred while querying the database.
+     */
+    final boolean getAllCodes(final Collection<Integer> addTo) throws 
SQLException {
+        boolean changed = false;
+        synchronized (factory) {
+            int code;
+            for (int index=0; (code = getCodeAt(index)) >= 0; index++) {
+                changed |= addTo.add(code);
+            }
+        }
+        return changed;
+    }
+
     /**
      * Returns the code at the given index, or -1 if the index is out of 
bounds.
      *
@@ -182,8 +254,8 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
         synchronized (factory) {
             if (codes == null) {
                 codes = new IntegerList(100, MAX_CODE);
-                results = (statements[ALL] = 
factory.connection.createStatement()).executeQuery(sql[ALL]);
-                sql[ALL] = null;                // Not needed anymore.
+                results = (statements[ALL_CODES] = 
factory.connection.createStatement()).executeQuery(sql[ALL_CODES]);
+                sql[ALL_CODES] = null;          // Not needed anymore.
             }
             int more = index - codes.size();    // Positive as long as we need 
more data.
             if (more < 0) {
@@ -196,8 +268,8 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
                     if (!r.next()) {
                         results = null;
                         r.close();
-                        statements[ALL].close();
-                        statements[ALL] = null;
+                        statements[ALL_CODES].close();
+                        statements[ALL_CODES] = null;
                         return -1;
                     }
                     code = r.getInt(1);
@@ -245,7 +317,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      */
     @Override
     public String get(final Object code) {
-        if (code != null) {
+        if (code != null && statements.length > NAME_FOR_CODE) {
             final int n;
             if (code instanceof Number) {
                 n = ((Number) code).intValue();
@@ -256,11 +328,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
             }
             try {
                 synchronized (factory) {
-                    var statement = (PreparedStatement) statements[ONE];
-                    if (statement == null) {
-                        statements[ONE] = statement = 
factory.connection.prepareStatement(sql[ONE]);
-                        sql[ONE] = null;    // Not needed anymore.
-                    }
+                    final PreparedStatement statement = 
prepareStatement(NAME_FOR_CODE);
                     statement.setInt(1, n);
                     try (ResultSet r = statement.executeQuery()) {
                         while (r.next()) {
@@ -335,7 +403,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
     /**
      * Invoked when a SQL statement cannot be executed, or the result 
retrieved.
      */
-    private BackingStoreException factoryFailure(final SQLException exception) 
{
+    private static BackingStoreException factoryFailure(final SQLException 
exception) {
         return new BackingStoreException(exception.getLocalizedMessage(), 
exception);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java
index f317371de3..09ddda6381 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java
@@ -46,6 +46,12 @@ final class CloseableReference extends 
WeakReference<AuthorityCodes> implements
      */
     private final Statement[] statements;
 
+    /**
+     * Whether the referenced {@link AuthorityCodes} has been given to the 
user.
+     * If {@code false}, we can invoke {@link #close()} without waiting for 
the garbage collection.
+     */
+    boolean published;
+
     /**
      * Creates a new phantom reference which will close the given statements
      * when the given referenced object will be garbage collected.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
index 690cb585ef..0c0df29970 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
@@ -18,7 +18,10 @@ package org.apache.sis.referencing.factory.sql;
 
 import java.util.Set;
 import java.util.List;
+import java.util.ArrayList;
 import java.util.LinkedHashSet;
+import java.util.Collection;
+import java.util.Iterator;
 import java.sql.Statement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -32,16 +35,21 @@ import org.opengis.referencing.crs.CompoundCRS;
 import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.crs.VerticalCRS;
+import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.datum.Datum;
 import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.TemporalDatum;
 import org.opengis.referencing.datum.VerticalDatum;
+import org.opengis.referencing.datum.EngineeringDatum;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.CollectionsExt;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.pending.jdk.JDK16;
 import org.apache.sis.pending.jdk.JDK19;
 import org.apache.sis.metadata.privy.ReferencingServices;
@@ -49,10 +57,15 @@ import org.apache.sis.metadata.sql.privy.SQLUtilities;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.privy.Formulas;
+import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.referencing.factory.ConcurrentAuthorityFactory;
 import static 
org.apache.sis.metadata.privy.NameToIdentifier.Simplifier.ESRI_DATUM_PREFIX;
 
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.referencing.crs.ParametricCRS;
+import org.opengis.referencing.datum.ParametricDatum;
+
 // Specific to the geoapi-4.0 branch:
 import org.opengis.referencing.crs.DerivedCRS;
 
@@ -74,7 +87,7 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder {
     /**
      * The type of object to search, or {@code null} for using {@code 
object.getClass()}.
      * This is set to a non-null value when searching for dependencies, in 
order to avoid
-     * confusion in an implementation class implements more than one GeoAPI 
interfaces.
+     * confusion if an implementation class implements more than one GeoAPI 
interfaces.
      *
      * @see #isInstance(Class, IdentifiedObject)
      */
@@ -103,6 +116,20 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder {
         }
     }
 
+    /**
+     * Returns the name of the given object, with a preference for 
<abbr>EPSG</abbr> name.
+     *
+     * @param  object  the object for which to get a name.
+     * @return the object name, or {@code null} if none.
+     */
+    private static String getName(final IdentifiedObject object) {
+        String name = IdentifiedObjects.getName(object, Citations.EPSG);
+        if (name == null) {
+            name = IdentifiedObjects.getName(object, null);
+        }
+        return name;
+    }
+
     /**
      * Returns a description of the condition to put in a {@code WHERE} clause 
for an object having
      * the given dependency.
@@ -139,7 +166,7 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder {
             final Set<Number> filters = JDK19.newLinkedHashSet(find.size());
             for (final IdentifiedObject dep : find) {
                 Identifier id = IdentifiedObjects.getIdentifier(dep, 
Citations.EPSG);
-                if (id != null) try {                                          
         // Should never be null, but let be safe.
+                if (id != null) try {
                     filters.add(Integer.valueOf(id.getCode()));
                 } catch (NumberFormatException e) {
                     Logging.recoverableException(EPSGDataAccess.LOGGER, 
EPSGCodeFinder.class, "getCodeCandidates", e);
@@ -271,14 +298,19 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder 
{
     }
 
     /**
-     * Returns a set of authority codes that <strong>may</strong> identify the 
same object as the specified one.
-     * This implementation tries to get a smaller set than what {@link 
EPSGDataAccess#getAuthorityCodes(Class)}
-     * would produce. Deprecated objects must be last in iteration order.
+     * Adds in the given collection the authority codes that 
<strong>may</strong> identify the same object as the specified one.
+     * This implementation tries to get a smaller set than what {@link 
EPSGDataAccess#getAuthorityCodes(Class)} would produce.
+     * Deprecated objects must be last in iteration order.
+     *
+     * @param  object  the object to search in the database.
+     * @param  all     whether to include the codes of all objects, even 
deprecated.
+     * @param  addTo   where to add the codes of objects that have been found.
+     * @return whether at least one code has been added.
+     * @throws FactoryException if an error occurred while fetching the set of 
code candidates.
      */
-    @Override
-    protected Set<String> getCodeCandidates(final IdentifiedObject object) 
throws FactoryException {
-        final TableInfo   table;                        // Contains 
`codeColumn` and `table` names.
-        final Condition[] filters;                      // Conditions to put 
in the WHERE clause.
+    private boolean searchCodesFromProperties(final IdentifiedObject object, 
final boolean all, final Collection<Integer> addTo) throws FactoryException {
+        final TableInfo   source;       // Contains `codeColumn` and `table` 
names.
+        final Condition[] filters;      // Conditions to put in the WHERE 
clause.
 crs:    if (isInstance(CoordinateReferenceSystem.class, object)) {
             /*
              * For compound CRS, the SQL statement may be something like below
@@ -288,24 +320,26 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
              *       AND CMPD_HORIZCRS_CODE IN (?,…)
              *       AND CMPD_VERTCRS_CODE IN (?,…)
              */
-            table = TableInfo.CRS;
+            source = TableInfo.CRS;
             if (isInstance(CompoundCRS.class, object)) {
                 final List<CoordinateReferenceSystem> components = 
((CompoundCRS) object).getComponents();
                 if (components != null) {       // Paranoiac check.
-                    final int n = components.size();
-                    if (n == 2) {
-                        filters = new Condition[2];
-                        for (int i=0; i<=1; i++) {
-                            if ((filters[i] = dependencies((i == 0) ? 
"CMPD_HORIZCRS_CODE" : "CMPD_VERTCRS_CODE",
-                                    CoordinateReferenceSystem.class, 
components.get(i), false)) == null)
-                            {
-                                return Set.of();
+                    switch (components.size()) {
+                        case 1: {
+                            // Defined for safety, but should not happen.
+                            return 
searchCodesFromProperties(components.get(0), all, addTo);
+                        }
+                        case 2: {
+                            filters = new Condition[2];
+                            for (int i=0; i<=1; i++) {
+                                final CoordinateReferenceSystem component = 
components.get(i);
+                                final String column = (i == 0) ? 
"CMPD_HORIZCRS_CODE" : "CMPD_VERTCRS_CODE";
+                                if ((filters[i] = dependencies(column, 
CoordinateReferenceSystem.class, component, false)) == null) {
+                                    return false;
+                                }
                             }
+                            break crs;
                         }
-                        break crs;
-                    }
-                    if (n == 1) {               // Should not happen.
-                        return getCodeCandidates(components.get(0));
                     }
                 }
             }
@@ -322,18 +356,22 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
             if (object instanceof DerivedCRS) {              // No need to use 
isInstance(Class, Object) from here.
                 filter = dependencies("BASE_CRS_CODE", SingleCRS.class, 
((DerivedCRS) object).getBaseCRS(), true);
             } else if (object instanceof GeodeticCRS) {
-                filter = dependencies("DATUM_CODE", GeodeticDatum.class, 
((GeodeticCRS) object).getDatum(), true);
+                filter = dependencies("DATUM_CODE", GeodeticDatum.class, 
DatumOrEnsemble.asDatum((GeodeticCRS) object), true);
             } else if (object instanceof VerticalCRS) {
-                filter = dependencies("DATUM_CODE", VerticalDatum.class, 
((VerticalCRS) object).getDatum(), true);
+                filter = dependencies("DATUM_CODE", VerticalDatum.class, 
DatumOrEnsemble.asDatum((VerticalCRS) object), true);
             } else if (object instanceof TemporalCRS) {
-                filter = dependencies("DATUM_CODE", TemporalDatum.class, 
((TemporalCRS) object).getDatum(), true);
+                filter = dependencies("DATUM_CODE", TemporalDatum.class, 
DatumOrEnsemble.asDatum((TemporalCRS) object), true);
+            } else if (object instanceof ParametricCRS) {
+                filter = dependencies("DATUM_CODE", ParametricDatum.class, 
DatumOrEnsemble.asDatum((ParametricCRS) object), true);
+            } else if (object instanceof EngineeringCRS) {
+                filter = dependencies("DATUM_CODE", EngineeringDatum.class, 
DatumOrEnsemble.asDatum((EngineeringCRS) object), true);
             } else if (object instanceof SingleCRS) {
                 filter = dependencies("DATUM_CODE", Datum.class, ((SingleCRS) 
object).getDatum(), true);
             } else {
-                return Set.of();
+                return false;
             }
             if (filter == null) {
-                return Set.of();
+                return false;
             }
             filters = new Condition[] {filter};
         } else if (isInstance(Datum.class, object)) {
@@ -346,19 +384,15 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
              *    WHERE ELLIPSOID_CODE IN (?,…)
              *      AND (LOWER(DATUM_NAME) LIKE '?%')
              */
-            table = TableInfo.DATUM;
+            source = TableInfo.DATUM;
             if (isInstance(GeodeticDatum.class, object)) {
-                filters = new Condition[] {
-                    dependencies("ELLIPSOID_CODE", Ellipsoid.class, 
((GeodeticDatum) object).getEllipsoid(), true),
-                    Condition.NAME
-                };
-                if (filters[0] == null) {
-                    return Set.of();
+                Condition filter = dependencies("ELLIPSOID_CODE", 
Ellipsoid.class, ((GeodeticDatum) object).getEllipsoid(), true);
+                if (filter == null) {
+                    return false;
                 }
+                filters = new Condition[] {filter, Condition.NAME};
             } else {
-                filters = new Condition[] {
-                    Condition.NAME
-                };
+                filters = new Condition[] {Condition.NAME};
             }
         } else if (isInstance(Ellipsoid.class, object)) {
             /*
@@ -368,13 +402,15 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
              *     WHERE SEMI_MAJOR_AXIS >= ?-ε AND SEMI_MAJOR_AXIS <= ?+ε
              *     ORDER BY ABS(SEMI_MAJOR_AXIS-?)
              */
-            table   = TableInfo.ELLIPSOID;
+            source  = TableInfo.ELLIPSOID;
             filters = new Condition[] {
                 new FloatCondition("SEMI_MAJOR_AXIS", ((Ellipsoid) 
object).getSemiMajorAxis())
             };
-        } else {
-            // Not a supported type. Returns all codes.
-            return super.getCodeCandidates(object);
+        } else try {
+            // Not a supported type. Returns all codes if not too expensive.
+            return dao.getAuthorityCodes(declaredType, addTo);
+        } catch (SQLException exception) {
+            throw databaseFailure(exception);
         }
         /*
          * At this point we collected the information needed for creating the 
main SQL query.
@@ -388,13 +424,13 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         final String aliasSQL;
         if (ArraysExt.containsIdentity(filters, Condition.NAME)) {
             namePatterns = new LinkedHashSet<>();
-            namePatterns.add(toDatumPattern(object.getName().getCode(), 
buffer));
+            namePatterns.add(toDatumPattern(getName(object), buffer));
             for (final GenericName id : object.getAlias()) {
                 namePatterns.add(toDatumPattern(id.tip().toString(), buffer));
             }
             buffer.setLength(0);
             buffer.append("SELECT OBJECT_CODE FROM \"Alias\" WHERE 
OBJECT_TABLE_NAME='")
-                  .append(dao.translator.toActualTableName(table.unquoted()))
+                  .append(dao.translator.toActualTableName(source.table))
                   .append("' AND ");
             // PostgreSQL does not require explicit cast when the value is a 
literal instead of "?".
             appendFilterByName(namePatterns, "ALIAS", buffer);
@@ -417,8 +453,8 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
          * of dependencies or parameter values as floating points. The last 
condition is on the object name.
          * It may be absent (typically, only datums or reference frames have 
that condition).
          */
-        buffer.append("SELECT ").append(table.codeColumn).append(" FROM 
").append(table.table);
-        table.where(dao, object, buffer);           // Unconditionally append 
a "WHERE" clause.
+        buffer.append("SELECT ").append(source.codeColumn).append(" FROM 
").append(source.fromClause);
+        source.where(dao, object, buffer);          // Unconditionally append 
a "WHERE" clause.
         boolean isNext = false;
         for (final Condition filter : filters) {
             isNext |= filter.appendToWhere(buffer, isNext);
@@ -432,14 +468,14 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
             if (namePatterns != null) {
                 if (isNext) buffer.append(" AND ");
                 isNext = false;
-                appendFilterByName(namePatterns, table.nameColumn, buffer);
+                appendFilterByName(namePatterns, source.nameColumn, buffer);
                 try (ResultSet result = stmt.executeQuery(aliasSQL)) {
                     while (result.next()) {
                         final int code = result.getInt(1);
                         if (!result.wasNull()) {            // Should never be 
null but we are paranoiac.
                             if (!isNext) {
                                 isNext = true;
-                                buffer.append(" OR 
").append(table.codeColumn).append(" IN (");
+                                buffer.append(" OR 
").append(source.codeColumn).append(" IN (");
                             } else {
                                 buffer.append(',');
                             }
@@ -449,7 +485,6 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
                 }
                 if (isNext) buffer.append(')');
             }
-            final boolean all = (getSearchDomain() == Domain.ALL_DATASET);
             if (!all) {
                 buffer.append(" AND DEPRECATED=FALSE");
                 // Do not put spaces around "=" because SQLTranslator searches 
for this exact match.
@@ -461,28 +496,36 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
             for (final Condition filter : filters) {
                 filter.appendToOrderBy(buffer);
             }
-            buffer.append(table.codeColumn);          // Only for making order 
determinist.
+            buffer.append(source.codeColumn);         // Only for making order 
determinist.
             /*
              * At this point the SQL query is complete. Run it, preserving 
order.
              * Then sort the result by taking in account the supersession 
table.
              */
-            final var result = new LinkedHashSet<String>();     // We need to 
preserve order in this set.
-            try (ResultSet r = 
stmt.executeQuery(dao.translator.apply(buffer.toString()))) {
-                while (r.next()) {
-                    result.add(r.getString(1));
+            try (ResultSet result = 
stmt.executeQuery(dao.translator.apply(buffer.toString()))) {
+                while (result.next()) {
+                    final int code = result.getInt(1);
+                    if (!result.wasNull()) {    // Should never be null in a 
valid EPSG schema.
+                        addTo.add(code);
+                    }
                 }
             }
-            result.remove(null);    // Should not have null element, but let 
be safe.
-            dao.sort(table.unquoted(), result).ifPresent((sorted) -> {
-                result.clear();
-                result.addAll(JDK16.toList(sorted));
+            dao.sort(source.table, addTo, 
Integer::intValue).ifPresent((sorted) -> {
+                addTo.clear();
+                addTo.addAll(JDK16.toList(sorted.mapToObj(Integer::valueOf)));
             });
-            return result;
+            return true;
         } catch (SQLException exception) {
             throw dao.databaseFailure(Identifier.class, 
String.valueOf(CollectionsExt.first(filters[0].values)), exception);
         }
     }
 
+    /**
+     * Returns the exception to throw when a database error occurred for no 
particular <abbr>EPSG</abbr> code.
+     */
+    private static FactoryException databaseFailure(final SQLException 
exception) {
+        return new FactoryException(exception.getLocalizedMessage(), 
Exceptions.unwrap(exception));
+    }
+
     /**
      * Returns a SQL pattern for the given datum name. The name is returned in 
all lower cases for allowing
      * case-insensitive searches. Punctuations are replaced by any sequence of 
characters ({@code '%'}) and
@@ -496,7 +539,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
      *
      * @see 
org.apache.sis.referencing.datum.DefaultGeodeticDatum#isHeuristicMatchForName(String)
      */
-    private static String toDatumPattern(final String name, final 
StringBuilder buffer) {
+    private String toDatumPattern(final String name, final StringBuilder 
buffer) {
         int start = 0;
         if (name.startsWith(ESRI_DATUM_PREFIX)) {
             start = ESRI_DATUM_PREFIX.length();
@@ -505,7 +548,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         if (end < 0) end = name.length();
         end = CharSequences.skipTrailingWhitespaces(name, start, end);
         buffer.setLength(0);
-        SQLUtilities.toLikePattern(name, start, end, true, true, buffer);
+        SQLUtilities.toLikePattern(name, start, end, true, true, 
dao.translator.wildcardEscape, buffer);
         return buffer.toString();
     }
 
@@ -532,4 +575,188 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         }
         buffer.append(')');
     }
+
+    /**
+     * Returns a set of authority codes that <strong>may</strong> identify the 
same object as the specified one.
+     * This implementation tries to get a smaller set than what {@link 
EPSGDataAccess#getAuthorityCodes(Class)}
+     * would produce. Deprecated objects must be last in iteration order.
+     *
+     * <h4>Exceptions during iteration</h4>
+     * An unchecked {@link BackingStoreException} may be thrown during the 
iteration
+     * if the action of fetching codes from database was delayed and that 
action failed.
+     * The exception cause may be {@link FactoryException} or {@link 
SQLException}.
+     *
+     * @param  object  the object to search in the database.
+     * @return codes of objects that may be the requested ones.
+     * @throws FactoryException if an error occurred while fetching the set of 
code candidates.
+     */
+    @Override
+    protected Iterable<String> getCodeCandidates(final IdentifiedObject 
object) throws FactoryException {
+        for (final TableInfo table : TableInfo.EPSG) {
+            if (table.type.isInstance(object)) try {
+                return new CodeCandidates(object, table);
+            } catch (SQLException exception) {
+                throw databaseFailure(exception);
+            }
+        }
+        return Set.of();
+    }
+
+    /**
+     * Set of authority codes that <strong>may</strong> identify the same 
object as the specified one.
+     * This collection returns the codes that can be obtained easily before 
the more expensive searches.
+     */
+    private final class CodeCandidates implements Iterable<String> {
+        /** The object to search. */
+        private final IdentifiedObject object;
+
+        /** Information about the tables of the object to search. */
+        private final TableInfo source;
+
+        /** Cache of codes found so far. */
+        private final Set<Integer> codes;
+
+        /** Whether to include the codes of all objects, even the deprecated 
ones. */
+        private boolean includeAll;
+
+        /** Whether to use only identifiers, name and aliases in the search. */
+        private boolean easySearch;
+
+        /**
+         * Algorithm used for filling the {@link #codes} collection so far.
+         * 0 = identifiers, 1 = name, 2 = aliases, 3 = search based on 
properties.
+         */
+        private byte searchMethod;
+
+        /**
+         * Creates a lazy collection of code candidates.
+         * This constructor loads immediately some codes in order to have an 
exception early in case of problem.
+         *
+         * @param  object  the object to search in the database.
+         * @param  source  information about the table where to search for the 
object.
+         * @throws SQLException if an error occurred while searching for codes 
associated to names.
+         * @throws FactoryException if an error occurred while fetching the 
set of code candidates.
+         */
+        CodeCandidates(final IdentifiedObject object, final TableInfo source) 
throws SQLException, FactoryException {
+            this.object = object;
+            this.source = source;
+            this.codes  = new LinkedHashSet<>();
+            switch (getSearchDomain()) {
+                case DECLARATION: easySearch = true; break;
+                case ALL_DATASET: includeAll = true; break;
+                case EXHAUSTIVE_VALID_DATASET: {
+                    // Skip the search methods based on identifiers, name or 
aliases.
+                    searchCodesFromProperties(object, false, codes);
+                    searchMethod = 3;
+                    return;
+                }
+            }
+            for (final Identifier id : object.getIdentifiers()) {
+                if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) try {
+                    codes.add(Integer.valueOf(id.getCode()));
+                } catch (NumberFormatException exception) {
+                    Logging.ignorableException(EPSGDataAccess.LOGGER, 
IdentifiedObjectFinder.class, "find", exception);
+                }
+            }
+            if (codes.isEmpty()) {
+                fetchMoreCodes(codes);
+            }
+        }
+
+        /**
+         * Populates the given collection with code candidates.
+         * This method tries less expansive search methods before to tries 
more expensive search methods.
+         *
+         * @param  addTo  an initially empty collection where to add the codes.
+         * @return whether at least one code has been added to the given 
collection.
+         * @throws SQLException if an error occurred while searching for codes 
associated to names.
+         * @throws FactoryException if an error occurred while fetching the 
set of code candidates.
+         */
+        private boolean fetchMoreCodes(final Collection<Integer> addTo) throws 
SQLException, FactoryException {
+            do {
+                switch (searchMethod) {
+                    case 0: findCodesFromName(false, addTo); break;     // 
Fetch codes for the name.
+                    case 1: findCodesFromName(true,  addTo); break;     // 
Fetch codes for the aliases.
+                    case 2: if (easySearch) break;                      // 
Search codes based on object properties.
+                            searchCodesFromProperties(object, includeAll, 
addTo);
+                            break;
+                    default: return false;
+                }
+                searchMethod++;
+            } while (addTo.isEmpty());
+            return true;
+        }
+
+        /**
+         * Adds the authority codes of all objects of the given name.
+         * Callers should update the {@link #searchMethod} flag accordingly.
+         *
+         * @param  alias  whether to search in the alias table rather than the 
main name.
+         * @param  codes  the collection where to add the codes that have been 
found.
+         * @throws SQLException if an error occurred while querying the 
database.
+         */
+        private void findCodesFromName(final boolean alias, final 
Collection<Integer> addTo) throws SQLException {
+            final String name = getName(object);
+            if (name != null) {     // Should never be null, but we are 
paranoiac.
+                dao.findCodesFromName(source.table, object.getClass(), name, 
alias, addTo);
+            }
+        }
+
+        /**
+         * Returns additional code candidates which were not yet returned by 
the iteration.
+         * This method uses the next search method which hasn't be tried.
+         *
+         * @return the additional codes.
+         * @throws BackingStoreException if an error occurred while fetching 
the set of code candidates.
+         */
+        private Iterator<Integer> fetchMoreCodes() {
+            final var addTo = new ArrayList<Integer>();
+            do {
+                try {
+                    if (!fetchMoreCodes(addTo)) break;
+                } catch (SQLException | FactoryException exception) {
+                    throw new BackingStoreException(exception);
+                }
+                for (Iterator<Integer> it = addTo.iterator(); it.hasNext();) {
+                    if (!codes.add(it.next())) {
+                        it.remove();    // Code has already be returned.
+                    }
+                }
+            } while (addTo.isEmpty());
+            return addTo.iterator();
+        }
+
+        /**
+         * Returns an iterator over the code candidates. The codes are cached:
+         * the should not be fetched again if a second iteration is executed.
+         *
+         * <h4>Limitation</h4>
+         * The current implementation does not support concurrent iterations, 
even in the same thread.
+         * This is okay for the usage that Apache <abbr>SIS</abbr> is making 
of this iterator.
+         */
+        @Override
+        public Iterator<String> iterator() {
+            return new Iterator<String>() {
+                /** Iterator over a subset of the codes. */
+                private Iterator<Integer> sources = codes.iterator();
+
+                /** Tests whether there is more codes to return. */
+                @Override public boolean hasNext() {
+                    if (sources.hasNext()) {
+                        return true;
+                    }
+                    sources = fetchMoreCodes();
+                    return sources.hasNext();
+                }
+
+                /** Returns the next code. */
+                @Override public String next() {
+                    if (!sources.hasNext()) {
+                        sources = fetchMoreCodes();
+                    }
+                    return sources.next().toString();
+                }
+            };
+        }
+    }
 }
diff --git 
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
index c06174587d..5158226b41 100644
--- 
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
@@ -21,7 +21,6 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -30,7 +29,8 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.stream.Stream;
+import java.util.stream.IntStream;
+import java.util.function.ToIntFunction;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.LogRecord;
@@ -219,12 +219,6 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      */
     private final NameSpace namespace;
 
-    /**
-     * The last table in which object name were looked for.
-     * This is for internal use by {@link #toPrimaryKeys} only.
-     */
-    private String lastTableForName;
-
     /**
      * A pool of prepared statements. Keys are {@link String} objects related 
to their originating method
      * (for example "Ellipsoid" for {@link #createEllipsoid(String)}).
@@ -540,7 +534,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * for a deprecated object, even if that identifier does not show up in 
iterations.
      * In other words, the returned collection behaves as if deprecated codes 
were included in the set but invisible.
      *
-     * @param  type  the spatial reference objects type (may be {@code 
Object.class}).
+     * @param  type  the type of spatial reference objects for which to get 
the authority codes.
      * @return the set of authority codes for spatial reference objects of the 
given type (may be an empty set).
      * @throws FactoryException if access to the underlying database failed.
      */
@@ -550,85 +544,98 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
             if (connection.isClosed()) {
                 throw new 
FactoryException(error().getString(Errors.Keys.ConnectionClosed));
             }
-            return getCodeMap(type).keySet();
+            final AuthorityCodes codes = 
getCodeMap(Objects.requireNonNull(type), null, true);
+            if (codes != null) {
+                return codes.keySet();
+            }
         } catch (SQLException exception) {
             throw new FactoryException(exception.getLocalizedMessage(), 
Exceptions.unwrap(exception));
         }
+        return Set.of();
+    }
+
+    /**
+     * Puts all codes in the given collection. This method is used only as a 
fallback when {@link EPSGCodeFinder}
+     * cannot get a smaller list of authority codes by using {@code WHERE} 
conditions on property values.
+     * This method should not be invoked for the most common objects such as 
<abbr>CRS</abbr> and datum.
+     * This method may do nothing if getting all codes would be too expensive
+     * (especially since the caller would instantiate all enumerated objects).
+     *
+     * @param  type   the spatial reference objects type, or {@code null} if 
unspecified.
+     * @param  addTo  the collection where to add all codes.
+     * @return whether the collection has changed as a result of this method 
call.
+     * @throws SQLException if an error occurred while querying the database.
+     */
+    final boolean getAuthorityCodes(final Class<? extends IdentifiedObject> 
type, final Collection<Integer> addTo) throws SQLException {
+        if (type != null) {
+            final AuthorityCodes codes = getCodeMap(type, null, false);
+            if (codes != null) {
+                return codes.getAllCodes(addTo);
+            }
+        }
+        return false;
     }
 
     /**
      * Returns a map of <abbr>EPSG</abbr> authority codes as keys and object 
names as values.
      * The cautions documented in {@link #getAuthorityCodes(Class)} apply also 
to this map.
+     * If the given type is unsupported or too generic, returns {@code null}.
      *
-     * @param  type  the spatial reference objects type (may be {@code 
Object.class}).
-     * @return the map of authority codes associated to their names. May be an 
empty map.
+     * @param  type     the spatial reference objects type.
+     * @param  source   the table from which to get the authority codes, or 
{@code null} for automatic.
+     * @param  publish  whether the returned authority codes will be given to 
a user outside this package.
+     * @return the map of authority codes associated to their names, or {@code 
null} if unsupported.
      * @throws FactoryException if access to the underlying database failed.
      *
      * @see #getAuthorityCodes(Class)
      * @see #getDescriptionText(Class, String)
      */
-    private synchronized Map<String,String> getCodeMap(final Class<?> type) 
throws SQLException {
+    private synchronized AuthorityCodes getCodeMap(final Class<?> type, 
TableInfo source, boolean publish) throws SQLException {
         CloseableReference reference = authorityCodes.get(type);
         if (reference != null) {
             AuthorityCodes existing = reference.get();
             if (existing != null) {
+                reference.published |= publish;
                 return existing;
             }
         }
-        Map<String, String> result = Map.of();
-        for (final TableInfo table : TableInfo.EPSG) {
-            /*
-             * We test `isAssignableFrom` in the two ways for catching the 
following use cases:
-             *
-             *  - `table.type.isAssignableFrom(type)`
-             *    is for the case where a table is for 
CoordinateReferenceSystem while the user type is some subtype
-             *    like GeographicCRS. The GeographicCRS need to be queried 
into the CoordinateReferenceSystem table.
-             *    An additional filter will be applied inside the 
AuthorityCodes class implementation.
-             *
-             *  - `type.isAssignableFrom(table.type)`
-             *    is for the case where the user type is IdentifiedObject or 
Object, in which case we basically want
-             *    to iterate through every tables.
-             */
-            if (table.type.isAssignableFrom(type) || 
type.isAssignableFrom(table.type)) {
-                /*
-                 * Maybe an instance already existed but was not found above 
because the user specified some
-                 * implementation class instead of an interface class. Before 
to return a newly created map,
-                 * check again in the cached maps using the type computed by 
AuthorityCodes itself.
-                 */
-                var codes = new AuthorityCodes(connection, table, type, this);
-                reference = authorityCodes.get(codes.type);
-                if (reference != null) {
-                    AuthorityCodes existing = reference.get();
-                    if (existing != null) {
-                        codes = existing;
-                    } else {
-                        reference = null;           // The weak reference is 
no longer valid.
-                    }
-                }
-                if (reference == null) {
-                    reference = codes.createReference();
-                    authorityCodes.put(codes.type, reference);
-                }
-                if (type != codes.type) {
-                    authorityCodes.put(type, reference);
-                }
-                /*
-                 * We now have the codes for a single type. Append with the 
codes of previous types, if any.
-                 * This usually happen only if the user asked for the 
IdentifiedObject type. Of course this
-                 * break all our effort to query the data only when first 
needed, but the user should ask
-                 * for more specific types.
-                 */
-                if (result.isEmpty()) {
-                    result = codes;
-                } else {
-                    if (result instanceof AuthorityCodes) {
-                        result = new LinkedHashMap<>(result);
+        if (source == null) {
+            for (TableInfo c : TableInfo.EPSG) {
+                if (c.type.isAssignableFrom(type)) {
+                    if (source != null) {
+                        return null;        // The specified type is too 
generic.
                     }
-                    result.putAll(codes);
+                    source = c;
                 }
             }
+            if (source == null) {
+                return null;                // The specified type is 
unsupported.
+            }
         }
-        return result;
+        AuthorityCodes codes = new AuthorityCodes(source, type, this);
+        /*
+         * Maybe an instance already existed but was not found above because 
the user specified some
+         * implementation class instead of an interface class. Before to 
return a newly created map,
+         * check again in the cached maps using the type computed by 
AuthorityCodes itself.
+         */
+        reference = authorityCodes.get(codes.type);
+        if (reference != null) {
+            AuthorityCodes existing = reference.get();
+            if (existing != null) {
+                codes = existing;
+            } else {
+                reference = null;           // The weak reference is no longer 
valid.
+            }
+        }
+        if (reference == null) {
+            reference = codes.createReference();
+            authorityCodes.put(codes.type, reference);
+        }
+        if (type != codes.type) {
+            authorityCodes.put(type, reference);
+        }
+        reference.published |= publish;
+        return codes;
     }
 
     /**
@@ -658,12 +665,11 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
             throws FactoryException
     {
         try {
-            for (final TableInfo table : TableInfo.EPSG) {
-                if (table.nameColumn != null && 
type.isAssignableFrom(table.type)) {
-                    final String text = getCodeMap(table.type).get(code);
-                    if (text != null) {
-                        return Optional.of(new 
SimpleInternationalString(text));
-                    }
+            final AuthorityCodes codes = 
getCodeMap(Objects.requireNonNull(type), null, false);
+            if (codes != null) {
+                final String text = codes.get(code);
+                if (text != null) {
+                    return Optional.of(new SimpleInternationalString(text));
                 }
             }
         } catch (SQLException | BackingStoreException exception) {
@@ -712,81 +718,37 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * Converts <abbr>EPSG</abbr> codes or <abbr>EPSG</abbr> names to the 
numerical identifiers (the primary keys).
      * This method can be seen as the converse of above {@link 
#getDescriptionText(Class, String)} method.
      *
-     * @param  table       the table where the code should appears, or {@code 
null} if {@code codeColumn} is null.
-     * @param  codeColumn  the column name for the codes, or {@code null} if 
none.
-     * @param  nameColumn  the column name for the names, or {@code null} if 
none.
-     * @param  codes       the codes or names to convert to primary keys, as 
an array of length 1 or 2.
+     * @param  table  the table where the code should appear, or {@code null} 
for no search by name.
+     * @param  codes  the codes or names to convert to primary keys, as an 
array of length 1 or 2.
      * @return the numerical identifiers (i.e. the table primary key values).
      * @throws SQLException if an error occurred while querying the database.
      * @throws FactoryDataException if code is a name and two distinct 
numerical codes match the name.
      * @throws NoSuchAuthorityCodeException if code is a name and no numerical 
code match the name.
      */
-    private int[] toPrimaryKeys(final String table, final String codeColumn, 
final String nameColumn, final String... codes)
-            throws SQLException, FactoryException
-    {
+    private int[] toPrimaryKeys(final String table, final String... codes) 
throws SQLException, FactoryException {
         final int[] primaryKeys = new int[codes.length];
-next:   for (int i=0; i<codes.length; i++) {
+        for (int i=0; i<codes.length; i++) {
             String code = codes[i];
-            if (codeColumn != null && nameColumn != null && 
!isPrimaryKey(code)) {
+            if (table != null && !isPrimaryKey(code)) {
                 /*
                  * The given string is not a numerical code. Search the value 
in the database.
                  * We search first in the table of the query. If the name is 
not found there,
                  * then we will search in the aliases table as a fallback.
                  */
-                final String pattern = SQLUtilities.toLikePattern(code, false);
-                boolean searchInTableOfQuery = true;
+                final var result = new ArrayList<Integer>();
+                findCodesFromName(table, null, code, false, result);
+                if (result.isEmpty()) {
+                    // Search in aliases only if no match was found in primary 
names.
+                    findCodesFromName(table, null, code, true, result);
+                }
                 Integer resolved = null;
-                do {    // Executed exactly 1 or 2 times.
-                    PreparedStatement stmt;
-                    if (searchInTableOfQuery) {
-                        /*
-                         * The SQL query for searching in the queried table is 
a little bit more complicated
-                         * than the query for searching in the alias table. 
The existing prepared statement
-                         * can be reused only if it was created for the 
current table.
-                         */
-                        final String KEY = "PrimaryKey";
-                        if (table.equals(lastTableForName)) {
-                            stmt = statements.get(KEY);
-                        } else {
-                            stmt = statements.remove(KEY);
-                            if (stmt != null) {
-                                stmt.close();
-                                stmt = null;
-                            }
-                        }
-                        if (stmt == null) {
-                            stmt = 
connection.prepareStatement(translator.apply(
-                                    "SELECT " + codeColumn + ", " + nameColumn
-                                            + " FROM \"" + table + '"'
-                                            + " WHERE " + nameColumn + " LIKE 
?"));
-                            statements.put(KEY, stmt);
-                            lastTableForName = table;
-                        }
-                        stmt.setString(1, pattern);
-                    } else {
-                        /*
-                         * If the object name is not found in the queries 
table,
-                         * search in the table of aliases.
-                         */
-                        stmt = prepareStatement("AliasKey",
-                                "SELECT OBJECT_CODE, ALIAS"
-                                        + " FROM \"Alias\""
-                                        + " WHERE OBJECT_TABLE_NAME=? AND 
ALIAS LIKE ?");
-                        stmt.setString(1, translator.toActualTableName(table));
-                        stmt.setString(2, pattern);
-                    }
-                    try (ResultSet result = stmt.executeQuery()) {
-                        while (result.next()) {
-                            if (SQLUtilities.filterFalsePositive(code, 
result.getString(2))) {
-                                resolved = 
ensureSingleton(getOptionalInteger(result, 1), resolved, code);
-                            }
-                        }
-                    }
-                    if (resolved != null) {
-                        primaryKeys[i] = resolved;
-                        continue next;
-                    }
-                } while ((searchInTableOfQuery = !searchInTableOfQuery) == 
false);
+                for (Integer value : result) {
+                    resolved = ensureSingleton(value, resolved, code);
+                }
+                if (resolved != null) {
+                    primaryKeys[i] = resolved;
+                    continue;
+                }
             }
             /*
              * At this point, `code` should be the primary key. It may still 
be a non-numerical string
@@ -805,6 +767,48 @@ next:   for (int i=0; i<codes.length; i++) {
         return primaryKeys;
     }
 
+    /**
+     * Finds the authority codes for the given name.
+     *
+     * @param  table  the table where the code should appear.
+     * @parma  type   the type of object to searh, or {@code null} for 
inferring from the table.
+     * @param  name   the name to search.
+     * @param  alias  whether to search in the alias table rather than the 
main name.
+     * @param  addTo  the collection where to add the codes that have been 
found.
+     * @throws SQLException if an error occurred while querying the database.
+     */
+    final void findCodesFromName(final String table, final Class<?> type, 
final String name, final boolean alias, final Collection<Integer> addTo)
+            throws SQLException
+    {
+        final String pattern = SQLUtilities.toLikePattern(name, false, 
translator.wildcardEscape);
+        if (alias) {
+            final PreparedStatement stmt = prepareStatement(
+                    "AliasKey",
+                    "SELECT OBJECT_CODE, ALIAS"
+                            + " FROM \"Alias\""
+                            + " WHERE OBJECT_TABLE_NAME=? AND ALIAS LIKE ?");
+            stmt.setString(1, translator.toActualTableName(table));
+            stmt.setString(2, pattern);
+            try (ResultSet result = stmt.executeQuery()) {
+                while (result.next()) {
+                    if (SQLUtilities.filterFalsePositive(name, 
result.getString(2))) {
+                        addTo.add(getOptionalInteger(result, 1));
+                    }
+                }
+            }
+        } else {
+            for (final TableInfo source : TableInfo.EPSG) {
+                if (table.equals(source.table)) {
+                    AuthorityCodes codes = getCodeMap(type == null ? 
source.type : type, source, false);
+                    if (codes != null) {
+                        codes.findCodesFromName(pattern, name, addTo);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Creates and executes a statement for the given codes with a protection 
against infinite loops.
      * The first code value is assigned to parameter #1, the second code value 
(if any) is assigned to parameter #2,
@@ -825,27 +829,21 @@ next:   for (int i=0; i<codes.length; i++) {
      *       in their {@code finally} block.</li>
      * </ul>
      *
-     * @param  table       the table where the code should appears.
-     * @param  codeColumn  the column name for the codes, or {@code null} if 
none.
-     * @param  nameColumn  the column name for the names, or {@code null} if 
none.
-     * @param  sql         the SQL statement to use for creating the {@link 
PreparedStatement} object.
-     *                     Will be used only if no prepared statement was 
already created for the given code.
-     * @param  codes       the codes of the object to create, as an array of 
length 1 or 2.
+     * @param  table  the table where the code should appears.
+     * @param  sql    the SQL statement to use for creating the {@link 
PreparedStatement} object.
+     *                Will be used only if no prepared statement was already 
created for the given code.
+     * @param  codes  the codes of the object to create, as an array of length 
1 or 2.
      * @return the result of the query.
      * @throws SQLException if an error occurred while querying the database.
      */
-    private ResultSet executeSingletonQuery(final String    table,
-                                            final String    codeColumn,
-                                            final String    nameColumn,
-                                            final String    sql,
-                                            final String... codes)
+    private ResultSet executeSingletonQuery(final String table, final String 
sql, final String... codes)
             throws SQLException, FactoryException
     {
         assert Thread.holdsLock(this);
         assert sql.contains('"' + table + '"') : table;
-        assert (codeColumn == null) || sql.contains(codeColumn) || 
table.equals("Extent") : codeColumn;
-        assert (nameColumn == null) || sql.contains(nameColumn) || 
table.equals("Extent") : nameColumn;
-        final int[] keys = toPrimaryKeys(table, codeColumn, nameColumn, codes);
+    //  assert (codeColumn == null) || sql.contains(codeColumn) || 
table.equals("Extent") : codeColumn;
+    //  assert (nameColumn == null) || sql.contains(nameColumn) || 
table.equals("Extent") : nameColumn;
+        final int[] keys = toPrimaryKeys(table, codes);
         currentSingletonQuery = new QueryID(table, keys, 
currentSingletonQuery);
         if (currentSingletonQuery.isAlreadyInProgress()) {
             throw new FactoryDataException(resources().getString(
@@ -1423,7 +1421,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final int queryStart = query.length();
         int found = -1;
         try {
-            final int key = isPrimaryKey ? toPrimaryKeys(null, null, null, 
code)[0] : 0;
+            final int key = isPrimaryKey ? toPrimaryKeys(null, code)[0] : 0;
             for (int i = 0; i < TableInfo.EPSG.length; i++) {
                 final TableInfo table = TableInfo.EPSG[i];
                 final String column = isPrimaryKey ? table.codeColumn : 
table.nameColumn;
@@ -1435,7 +1433,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                 if (!isPrimaryKey) {
                     query.append(", ").append(column);      // Only for 
filterFalsePositive(…).
                 }
-                query.append(" FROM ").append(table.table)
+                query.append(" FROM ").append(table.fromClause)
                      .append(" WHERE ").append(column).append(isPrimaryKey ? " 
= ?" : " LIKE ?");
                 try (PreparedStatement stmt = 
connection.prepareStatement(translator.apply(query.toString()))) {
                     /*
@@ -1445,7 +1443,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                     if (isPrimaryKey) {
                         stmt.setInt(1, key);
                     } else {
-                        stmt.setString(1, SQLUtilities.toLikePattern(code, 
false));
+                        stmt.setString(1, SQLUtilities.toLikePattern(code, 
false, translator.wildcardEscape));
                     }
                     Integer present = null;
                     try (ResultSet result = stmt.executeQuery()) {
@@ -1565,8 +1563,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Coordinate Reference System",
-                "COORD_REF_SYS_CODE",
-                "COORD_REF_SYS_NAME",
                 "SELECT"+ /* column  1 */ " COORD_REF_SYS_CODE,"
                         + /* column  2 */ " COORD_REF_SYS_NAME,"
                         + /* column  3 */ " AREA_OF_USE_CODE,"      // 
Deprecated since EPSG version 10 (always NULL)
@@ -1880,8 +1876,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Datum",
-                "DATUM_CODE",
-                "DATUM_NAME",
                 "SELECT"+ /* column  1 */ " DATUM_CODE,"
                         + /* column  2 */ " DATUM_NAME,"
                         + /* column  3 */ " DATUM_TYPE,"
@@ -2170,8 +2164,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Ellipsoid",
-                "ELLIPSOID_CODE",
-                "ELLIPSOID_NAME",
                 "SELECT"+ /* column 1 */ " ELLIPSOID_CODE,"
                         + /* column 2 */ " ELLIPSOID_NAME,"
                         + /* column 3 */ " SEMI_MAJOR_AXIS,"
@@ -2274,8 +2266,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Prime Meridian",
-                "PRIME_MERIDIAN_CODE",
-                "PRIME_MERIDIAN_NAME",
                 "SELECT"+ /* column 1 */ " PRIME_MERIDIAN_CODE,"
                         + /* column 2 */ " PRIME_MERIDIAN_NAME,"
                         + /* column 3 */ " GREENWICH_LONGITUDE,"
@@ -2355,8 +2345,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Extent",
-                "EXTENT_CODE",
-                "EXTENT_NAME",
                 "SELECT"+ /* column  1 */ " EXTENT_DESCRIPTION,"
                         + /* column  2 */ " BBOX_SOUTH_BOUND_LAT,"
                         + /* column  3 */ " BBOX_NORTH_BOUND_LAT,"
@@ -2474,8 +2462,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Coordinate System",
-                "COORD_SYS_CODE",
-                "COORD_SYS_NAME",
                 "SELECT"+ /* column 1 */ " COORD_SYS_CODE,"
                         + /* column 2 */ " COORD_SYS_NAME,"
                         + /* column 3 */ " COORD_SYS_TYPE,"
@@ -2637,8 +2623,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Coordinate Axis",
-                "COORD_AXIS_CODE",
-                null,
                 "SELECT"+ /* column 1 */ " COORD_AXIS_CODE,"
                         + /* column 2 */ " COORD_AXIS_NAME_CODE,"
                         + /* column 3 */ " COORD_AXIS_ORIENTATION,"
@@ -2779,8 +2763,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Unit of Measure",
-                "UOM_CODE",
-                "UNIT_OF_MEAS_NAME",
                 "SELECT"+ /* column 1 */ " UOM_CODE,"
                         + /* column 2 */ " FACTOR_B,"
                         + /* column 3 */ " FACTOR_C,"
@@ -2863,8 +2845,6 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Coordinate_Operation Parameter",
-                "PARAMETER_CODE",
-                "PARAMETER_NAME",
                 "SELECT"+ /* column 1 */ " PARAMETER_CODE,"
                         + /* column 2 */ " PARAMETER_NAME,"
                         + /* column 3 */ " DESCRIPTION,"
@@ -3115,8 +3095,6 @@ next:                   while (r.next()) {
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Coordinate_Operation Method",
-                "COORD_OP_METHOD_CODE",
-                "COORD_OP_METHOD_NAME",
                 "SELECT"+ /* column 1 */ " COORD_OP_METHOD_CODE,"
                         + /* column 2 */ " COORD_OP_METHOD_NAME,"
                         + /* column 3 */ " REMARKS,"
@@ -3193,8 +3171,6 @@ next:                   while (r.next()) {
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
                 "Coordinate_Operation",
-                "COORD_OP_CODE",
-                "COORD_OP_NAME",
                 "SELECT"+ /* column  1 */ " COORD_OP_CODE,"
                         + /* column  2 */ " COORD_OP_NAME,"
                         + /* column  3 */ " COORD_OP_TYPE,"
@@ -3385,7 +3361,7 @@ next:                   while (r.next()) {
         final String label = sourceCRS + " ⇨ " + targetCRS;
         final var set = new CoordinateOperationSet(owner);
         try {
-            final int[] pair = toPrimaryKeys(null, null, null, sourceCRS, 
targetCRS);
+            final int[] pair = toPrimaryKeys(null, sourceCRS, targetCRS);
             boolean searchTransformations = false;
             do {
                 /*
@@ -3422,9 +3398,9 @@ next:                   while (r.next()) {
              * Alter the ordering using the information supplied in the extents
              * and supersession tables.
              */
-            List<String> codes = Arrays.asList(set.getAuthorityCodes());
-            sort("Coordinate_Operation", codes).ifPresent((sorted) -> {
-                set.setAuthorityCodes(sorted.toArray(String[]::new));
+            final List<String> codes = Arrays.asList(set.getAuthorityCodes());
+            sort("Coordinate_Operation", codes, 
Integer::parseInt).ifPresent((sorted) -> {
+                
set.setAuthorityCodes(sorted.mapToObj(Integer::toString).toArray(String[]::new));
             });
         } catch (SQLException exception) {
             throw databaseFailure(CoordinateOperation.class, label, exception);
@@ -3468,9 +3444,10 @@ next:                   while (r.next()) {
      *
      * @param  table  the table of the objects for which to check for 
supersession.
      * @param  codes  the codes to sort. This collection will not be modified 
by this method.
-     * @return codes of sorted elements, or empty if this method did not 
changed the codes order.
+     * @param  parser the method to invoke for converting a {@code codes} 
element to an integer.
+     * @return codes of sorted elements, or empty if this method did not 
change the codes order.
      */
-    final synchronized Optional<Stream<String>> sort(final String table, final 
Collection<String> codes)
+    final synchronized <C extends Comparable<?>> Optional<IntStream> 
sort(final String table, final Collection<C> codes, final ToIntFunction<C> 
parser)
             throws SQLException, FactoryException
     {
         final int size = codes.size();
@@ -3479,10 +3456,10 @@ next:                   while (r.next()) {
             final var extents = new ArrayList<String>();
             final String actualTable = translator.toActualTableName(table);
             int count = 0;
-            for (final String code : codes) {
+            for (final C code : codes) {
                 final int key;
                 try {
-                    key = Integer.parseInt(code);
+                    key = parser.applyAsInt(code);
                 } catch (NumberFormatException e) {
                     unexpectedException("sort", e);
                     continue;
@@ -3497,7 +3474,7 @@ next:                   while (r.next()) {
                      * the finally block and the `TableInfo.areaOfUse` flag.
                      */
                     for (final TableInfo info : TableInfo.EPSG) {
-                        if (table.equals(info.unquoted())) {
+                        if (table.equals(info.table)) {
                             if (info.areaOfUse) {
                                 try (ResultSet result = executeQueryForCodes(
                                         "Area",     // Table from EPSG version 
9. Does not exist anymore in version 10.
@@ -3532,7 +3509,7 @@ next:                   while (r.next()) {
                 elements[count++] = element;
             }
             if (ObjectPertinence.sort(elements)) {
-                return 
Optional.of(Arrays.stream(elements).map(ObjectPertinence::code));
+                return Optional.of(Arrays.stream(elements).mapToInt((p) -> 
p.code));
             }
         } finally {
             /*
@@ -3603,11 +3580,22 @@ next:                   while (r.next()) {
      */
     final synchronized boolean canClose() {
         boolean can = true;
+        SQLException error = null;
         if (!authorityCodes.isEmpty()) {
             System.gc();                // For cleaning as much weak 
references as we can before we check them.
             final Iterator<CloseableReference> it = 
authorityCodes.values().iterator();
             while (it.hasNext()) {
-                if (JDK16.refersTo(it.next(), null)) {
+                final CloseableReference reference = it.next();
+                if (!reference.published) {
+                    it.remove();
+                    try {
+                        reference.close();
+                    } catch (SQLException e) {
+                        if (error == null) error = e;
+                        else error.addSuppressed(e);
+                    }
+                    reference.clear();
+                } else if (JDK16.refersTo(reference, null)) {
                     it.remove();
                 } else {
                     /*
@@ -3618,11 +3606,14 @@ next:                   while (r.next()) {
                 }
             }
         }
+        if (error != null) {
+            unexpectedException("canClose", error);
+        }
         return can;
     }
 
     /**
-     * Closes the JDBC connection used by this factory.
+     * Closes the <abbr>JDBC</abbr> connection used by this factory.
      * If this {@code EPSGDataAccess} is used by an {@link EPSGFactory}, then 
this method
      * will be automatically invoked after some {@linkplain 
EPSGFactory#getTimeout timeout}.
      *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
index 82da6646a7..f9c2c22860 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
@@ -47,7 +47,7 @@ final class ObjectPertinence implements 
Comparable<ObjectPertinence> {
     /**
      * Code of the object for which the pertinence is evaluated.
      */
-    private final int code;
+    final int code;
 
     /**
      * An estimation of the surface of the domain of validity as a negative 
number, or NaN if none.
@@ -85,13 +85,6 @@ final class ObjectPertinence implements 
Comparable<ObjectPertinence> {
         replacedBy = new ArrayList<>();
     }
 
-    /**
-     * Returns the code of the object for which the pertinence is evaluated.
-     */
-    final String code() {
-        return Integer.toString(code);
-    }
-
     /**
      * Determines the ordering based on the extent.
      * This method does not take supersession in account.
@@ -123,7 +116,7 @@ final class ObjectPertinence implements 
Comparable<ObjectPertinence> {
         do {
             redo = false;
             for (int i=0; i<elements.length; i++) {
-                for (final Integer replacement : elements[i].replacedBy) {
+                for (final int replacement : elements[i].replacedBy) {
                     for (int j=i+1; j<elements.length; j++) {
                         final ObjectPertinence candidate = elements[j];
                         if (candidate.code == replacement) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
index 1ec4515c09..b275964bab 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
@@ -214,11 +214,18 @@ public class SQLTranslator implements 
UnaryOperator<String> {
     private Map<String,String> replacements;
 
     /**
-     * The characters used for quoting identifiers, or a whitespace if none.
+     * The characters used for quoting identifiers, or an empty string if none.
      * This information is provided by {@link 
DatabaseMetaData#getIdentifierQuoteString()}.
      */
     private final String identifierQuote;
 
+    /**
+     * The string that can be used to escape wildcard characters in {@code 
LIKE}.
+     * This is the value returned by {@link 
DatabaseMetaData#getSearchStringEscape()}.
+     * It may be null or empty if the database has no escape character.
+     */
+    final String wildcardEscape;
+
     /**
      * Non-null if the {@value #ENUMERATION_COLUMN} column in {@code "Alias"} 
table uses enumeration instead
      * than character varying. In such case, this field contains the 
enumeration type. If {@code null}, then
@@ -283,6 +290,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
      */
     public SQLTranslator(final DatabaseMetaData md, final String catalog, 
final String schema) throws SQLException {
         identifierQuote = md.getIdentifierQuoteString().trim();
+        wildcardEscape  = md.getSearchStringEscape();
         this.catalog = catalog;
         this.schema  = schema;
         setup(md);
@@ -308,8 +316,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
      */
     @SuppressWarnings("fallthrough")
     final void setup(final DatabaseMetaData md) throws SQLException {
-        final String escape  = md.getSearchStringEscape();
-        String schemaPattern = SQLUtilities.escape(schema, escape);
+        String schemaPattern = SQLUtilities.escape(schema, wildcardEscape);
         int tableIndex = 0;
         do {
             usePrefixedTableNames  = false;
@@ -318,7 +325,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
             switch (tableIndex++) {
                 case 0: {   // Test EPSG standard table name first.
                     usePrefixedTableNames = true;
-                    table = SQLUtilities.escape(TABLE_PREFIX, escape);
+                    table = SQLUtilities.escape(TABLE_PREFIX, wildcardEscape);
                     // Fallthrough for testing "epsg_coordoperation".
                 }
                 case 2: {
@@ -352,7 +359,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
          * naming convention (unquoted or mixed-case, prefixed by "epsg_" or 
not).
          */
         UnaryOperator<String> toNativeCase = UnaryOperator.identity();
-        schemaPattern  = SQLUtilities.escape(schema, escape);
+        schemaPattern  = SQLUtilities.escape(schema, wildcardEscape);
         tableRewording = new HashMap<>();
         replacements   = new HashMap<>();
         /*
@@ -456,7 +463,7 @@ check:  for (;;) {
             boolean isTableFound = false;
             brokenTargetCols.addAll(mayRenameColumns.values());
             table = toNativeCase.apply(toActualTableName(table));
-            try (ResultSet result = md.getColumns(catalog, schemaPattern, 
SQLUtilities.escape(table, escape), "%")) {
+            try (ResultSet result = md.getColumns(catalog, schemaPattern, 
SQLUtilities.escape(table, wildcardEscape), "%")) {
                 while (result.next()) {
                     isTableFound = true;          // Assuming that all tables 
contain at least one column.
                     final String column = 
result.getString(Reflection.COLUMN_NAME).toUpperCase(Locale.US);
diff --git 
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
index bd68bcbf52..5acf36cebb 100644
--- 
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
@@ -169,13 +169,17 @@ final class TableInfo {
     final Class<?> type;
 
     /**
-     * The table name for SQL queries, including schema name and quotes for 
mixed-case names.
-     * May contain a {@code JOIN} clause.
-     *
-     * @see #unquoted()
+     * The table name in mixed-case and without quotes.
      */
     final String table;
 
+    /**
+     * The <abbr>SQL</abbr> fragment to use in the {@code FROM} clause.
+     * This is usually the table name, including the quotes for mixed-case 
names.
+     * In sometime, this is a more complex clause including {@code JOIN} 
statements.
+     */
+    final String fromClause;
+
     /**
      * Column name for the code (usually with the {@code "_CODE"} suffix).
      */
@@ -216,7 +220,7 @@ final class TableInfo {
      * Stores information about a specific table.
      *
      * @param type        the class of object to be created (usually a GeoAPI 
interface).
-     * @param table       the table name for SQL queries, including quotes for 
mixed-case names.
+     * @param fromClause  The <abbr>SQL</abbr> fragment to use in the {@code 
FROM} clause, including quotes.
      * @param codeColumn  column name for the code (usually with the {@code 
"_CODE"} suffix).
      * @param nameColumn  column name for the name (usually with the {@code 
"_NAME"} suffix), or {@code null}.
      * @param typeColumn  column type for the type (usually with the {@code 
"_TYPE"} suffix), or {@code null}.
@@ -226,12 +230,12 @@ final class TableInfo {
      * @param areaOfUse   whether the table had an {@code "AREA_OF_USE_CODE"} 
column in the legacy versions.
      */
     private TableInfo(final Class<?> type,
-                      final String table, final String codeColumn, final 
String nameColumn,
+                      final String fromClause, final String codeColumn, final 
String nameColumn,
                       final String typeColumn, final Class<?>[] subTypes, 
final String[] typeNames,
                       final String showColumn, final boolean areaOfUse)
     {
         this.type       = type;
-        this.table      = table;
+        this.fromClause = fromClause;
         this.codeColumn = codeColumn;
         this.nameColumn = nameColumn;
         this.typeColumn = typeColumn;
@@ -239,13 +243,7 @@ final class TableInfo {
         this.typeNames  = typeNames;
         this.showColumn = showColumn;
         this.areaOfUse  = areaOfUse;
-    }
-
-    /**
-     * Returns the table name without schema name and without quotes.
-     */
-    final String unquoted() {
-        return table.substring(table.indexOf('"') + 1, table.lastIndexOf('"'));
+        table = fromClause.substring(fromClause.indexOf('"') + 1, 
fromClause.lastIndexOf('"')).intern();
     }
 
     /**
@@ -263,7 +261,7 @@ final class TableInfo {
                 table = table.substring(SQLTranslator.TABLE_PREFIX.length());
             }
             for (final TableInfo info : EPSG) {
-                if (CharSequences.isAcronymForWords(table, info.unquoted())) {
+                if (CharSequences.isAcronymForWords(table, info.table)) {
                     return Optional.of(info.type.getSimpleName());
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index 2e9b18a75a..786face47f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -324,9 +324,8 @@ class CoordinateOperationRegistry {
             }
             codes = new ArrayList<>();
             codeFinder.setIgnoringAxes(true);
-            codeFinder.setSearchDomain(isEasySearch(crs)
-                    ? IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET
-                    : IdentifiedObjectFinder.Domain.VALID_DATASET);
+            codeFinder.setSearchDomain(isEasySearch(crs) ? 
IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET
+                                                         : 
IdentifiedObjectFinder.Domain.VALID_DATASET);
             int matchCount = 0;
             try {
                 final Citation authority = registry.getAuthority();
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java
index 6f7773c557..6f723ce1e3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java
@@ -107,7 +107,7 @@ public final class AuthorityFactoriesTest extends 
TestCaseWithLogs {
      * @param code      the code of the object for which to fetch the 
description.
      */
     private void assertDescriptionEquals(String expected, String code) throws 
FactoryException {
-        assertEquals(expected, 
AuthorityFactories.ALL.getDescriptionText(IdentifiedObject.class, 
code).orElseThrow().toString());
+        assertEquals(expected, 
AuthorityFactories.ALL.getDescriptionText(CoordinateReferenceSystem.class, 
code).orElseThrow().toString());
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
index 6543f29648..bf8525cc65 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
@@ -684,15 +684,6 @@ public final class EPSGFactoryTest extends 
TestCaseWithLogs {
         assertFalse(units.isEmpty());
         assertTrue (units.size() >= 2);
 
-        // Tests the fusion of all types
-        if (RUN_EXTENSIVE_TESTS) {
-            final Set<String> all = 
factory.getAuthorityCodes(IdentifiedObject.class);
-            assertTrue(all.containsAll(crs));
-            assertTrue(all.containsAll(datum));
-            assertTrue(all.containsAll(operations));
-            assertTrue(all.containsAll(units));
-        }
-
         // Try a dummy type.
         @SuppressWarnings({"unchecked","rawtypes"})
         final Class<? extends IdentifiedObject> wrong = (Class) String.class;
@@ -708,11 +699,11 @@ public final class EPSGFactoryTest extends 
TestCaseWithLogs {
     @Test
     public void testDescriptionText() throws FactoryException {
         final EPSGFactory factory = dataEPSG.factory();
-        assertEquals("World Geodetic System 1984", 
factory.getDescriptionText(IdentifiedObject.class,  
"6326").get().toString(Locale.US));
-        assertEquals("Mean Sea Level",             
factory.getDescriptionText(IdentifiedObject.class,  
"5100").get().toString(Locale.US));
-        assertEquals("NTF (Paris) / Nord France",  
factory.getDescriptionText(IdentifiedObject.class, 
"27591").get().toString(Locale.US));
-        assertEquals("NTF (Paris) / France II",    
factory.getDescriptionText(IdentifiedObject.class, 
"27582").get().toString(Locale.US));
-        assertEquals("Ellipsoidal height",         
factory.getDescriptionText(IdentifiedObject.class,    
"84").get().toString(Locale.US));
+        assertEquals("World Geodetic System 1984", 
factory.getDescriptionText(GeodeticDatum.class,      
"6326").get().toString(Locale.US));
+        assertEquals("Mean Sea Level",             
factory.getDescriptionText(VerticalDatum.class,      
"5100").get().toString(Locale.US));
+        assertEquals("NTF (Paris) / Nord France",  
factory.getDescriptionText(ProjectedCRS.class,      
"27591").get().toString(Locale.US));
+        assertEquals("NTF (Paris) / France II",    
factory.getDescriptionText(ProjectedCRS.class,      
"27582").get().toString(Locale.US));
+        assertEquals("Ellipsoidal height",         
factory.getDescriptionText(CoordinateSystemAxis.class, 
"84").get().toString(Locale.US));
         loggings.assertNoUnexpectedLog();
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
index ad0bb48804..af7647a142 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
@@ -816,7 +816,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
                     sridFromCRS = prepareSearchCRS(true);
                 }
                 sridFromCRS.setInt(1, code);
-                sridFromCRS.setString(2, SQLUtilities.toLikePattern(authority, 
true));
+                sridFromCRS.setString(2, SQLUtilities.toLikePattern(authority, 
true, database.wildcardEscape));
                 try (ResultSet result = sridFromCRS.executeQuery()) {
                     while (result.next()) {
                         if (SQLUtilities.filterFalsePositive(authority, 
result.getString(1))) {
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
index a69a986a26..d964ed27e9 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java
@@ -356,7 +356,7 @@ public final class Logging extends Static {
      * @since 1.0
      */
     public static boolean ignorableException(final Logger logger, final 
Class<?> classe,
-                                               final String method, final 
Throwable error)
+                                             final String method, final 
Throwable error)
     {
         final String classname = (classe != null) ? classe.getName() : null;
         return unexpectedException(logger, classname, method, error, 
Level.FINER);
diff --git 
a/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql
 
b/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql
index 39e59a71d5..9083d21524 100644
--- 
a/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql
+++ 
b/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql
@@ -10,8 +10,8 @@
 -- If enumerated values are not supported by the database, Apache SIS will 
automatically replace their usage
 -- by the VARCHAR type.
 --
-CREATE TYPE "Datum Kind"        AS ENUM ('geodetic', 'vertical', 'temporal', 
'engineering', 'dynamic geodetic', 'ensemble');
-CREATE TYPE "CRS Kind"          AS ENUM ('geocentric', 'geographic 2D', 
'geographic 3D', 'projected', 'vertical', 'temporal', 'compound', 
'engineering', 'derived');
+CREATE TYPE "Datum Kind"        AS ENUM ('geodetic', 'vertical', 'temporal', 
'parametric', 'engineering', 'dynamic geodetic', 'ensemble');
+CREATE TYPE "CRS Kind"          AS ENUM ('geocentric', 'geographic 2D', 
'geographic 3D', 'projected', 'vertical', 'temporal', 'parametric', 
'engineering', 'derived', 'compound');
 CREATE TYPE "CS Kind"           AS ENUM ('ellipsoidal', 'spherical', 
'Cartesian', 'vertical', 'gravity-related', 'time', 'linear', 'polar', 
'cylindrical', 'affine', 'ordinal');
 CREATE TYPE "Supersession Type" AS ENUM ('Supersession');
 CREATE TYPE "Table Name"        AS ENUM

Reply via email to