Skip to content

Fix missing CRS in custom search index results - #1920

Open
camende wants to merge 6 commits into
Tailormap:mainfrom
camende:fix/search-index-result-projection
Open

camende wants to merge 6 commits into
Tailormap:mainfrom
camende:fix/search-index-result-projection

Conversation

@camende

@camende camende commented Sep 23, 2026 •

Copy link
Copy Markdown

Problem

When using a Tailormap search index for a feature type whose CRS differs from the application map CRS, selecting a search result can zoom the map to an incorrect location.

We encountered this with:

  • Feature type geometry: EPSG:4326
  • Application map: EPSG:3857
  • Search result geometry stored in Solr: POINT (3.7651071548461914 51.742008209228516)

Search index geometries are stored in the native CRS of the feature type. Previously, the search endpoint returned that geometry unchanged, so it could be interpreted as if it were already in the application CRS.

Spatial search using pt and d had the same CRS issue: pt is supplied in the application CRS, while the Solr geometry is stored in the source CRS.

Solution

This change keeps the CRS handling server-side.

  • Adds source_crs to search_index using a Flyway migration.
  • Stores the feature type CRS in SearchIndex.sourceCrs when the search index is built.
  • Transforms search result geometries from the stored source CRS to the application CRS before returning them from the API.
  • Transforms the spatial-search pt value from the application CRS to the source CRS before sending the query to Solr.
  • Leaves d unchanged because it is already expressed in the distance units configured for the Solr geometry field.
  • Existing search indexes without a stored sourceCrs retain the previous behaviour until they are rebuilt.

The source CRS therefore does not need to be exposed through the viewer API, and no viewer-side CRS handling is required for this fix.

Testing

  • API compilation succeeds.
  • All 89 unit tests pass.
  • Added 3 focused SolrHelperTest tests covering:
    • transformation of spatial pt and result geometry when source and application CRS differ;
    • unchanged d value;
    • no transformation when source and application CRS are equal;
    • backwards-compatible behaviour when sourceCrs is not yet stored.
  • The original issue was reproduced with a PostGIS point feature type in EPSG:4326 and a Tailormap application using EPSG:3857.

camende and others added 5 commits September 22, 2026 19:40
Added projectionCode field to geometry definition.
Added feature type repository and feature source factory helper to the SearchController. Updated constructor and search logic to utilize feature type information.

@mprins mprins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer storing the source data CRS in the SearchIndex object when the index is created (this will require add a column using a new update script under https://github.com/Tailormap/tailormap-api/tree/main/src/main/resources/db/migration)

Also the re-projection/translation should be done server-side so that the source data projection is not need to be exposed. We do the same thing for the eg features and editing endpoints; the API should deliver geometry in the application/map CRS

The search endpoint will also need to be fixed with regard to the search-withing-distance option, see eg. the testcase get_spatial_query_distance that demonstrates the pt and d options

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The datastore resource leak must be resolved before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Low severity

Open (2)
What changed in this PR

Adds the feature type CRS to search results so viewers can correctly transform geometries.

Changes:

  • Adds nullable projectionCode to the OpenAPI schema.
  • Resolves and propagates the feature type CRS in Solr search results.
  • Passes CRS metadata through search handling.
File Summary Findings
src/​main/​resources/​openapi/​viewer-api.yaml Documents projectionCode. None.
src/​main/​java/​org/​tailormap/​api/​solr/​SolrHelper.java Adds CRS data to search documents. Nit (4 votes): Add regression coverage asserting projectionCode.
src/​main/​java/​org/​tailormap/​api/​controller/​SearchController.java Resolves the feature-type CRS. Critical (4 votes): Dispose the datastore after extracting the CRS to prevent resource leaks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +110 to +121
final String projectionCode;

try {
SimpleFeatureSource featureSource = featureSourceFactoryHelper.openGeoToolsFeatureSource(featureType);

projectionCode =
GeoToolsHelper.crsToString(featureSource.getSchema().getCoordinateReferenceSystem());
} catch (IOException e) {
logger.error("Unable to determine CRS for search index '{}'", searchIndex.getName(), e);
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "Unable to determine CRS for search index", e);
}
searchResponse.addDocumentsItem(new SearchDocument()
.fid(solrDocument.getFieldValue(SEARCH_ID_FIELD).toString())
.geometry(geom.toString())
.projectionCode(projectionCode)
@github-actions

Copy link
Copy Markdown

Test Results

0 tests   - 678   0 ✅  - 674   0s ⏱️ - 10m 15s
0 suites  -  66   0 💤  -   1 
0 files    -  66   0 ❌  -   3 

Results for commit fcd6e8d. ± Comparison against base commit b8c1b61.

@camende

camende commented Sep 24, 2026

Copy link
Copy Markdown
Author

Thanks for the review. I've reworked the implementation based on your feedback in commit 4449086.

The source CRS is now stored in SearchIndex when the index is built, search result geometries are reprojected server-side to the application CRS, and the spatial pt parameter is transformed back to the source CRS while keeping d unchanged.

I've also added focused unit tests covering differing/equal CRS and existing indexes without a stored source CRS.

The PR description has been updated to reflect the new implementation.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants