11/*
2- * Copyright © 2018-2025 Commonwealth Scientific and Industrial Research
2+ * Copyright © 2018-2026 Commonwealth Scientific and Industrial Research
33 * Organisation (CSIRO) ABN 41 687 119 230.
44 *
55 * Licensed under the Apache License, Version 2.0 (the "License");
2121
2222import au .csiro .pathling .fhirpath .FhirPath ;
2323import au .csiro .pathling .fhirpath .collection .Collection ;
24- import au .csiro .pathling .fhirpath .column .ColumnRepresentation ;
2524import au .csiro .pathling .fhirpath .evaluation .CrossResourceStrategy ;
2625import au .csiro .pathling .fhirpath .evaluation .SingleResourceEvaluator ;
2726import au .csiro .pathling .fhirpath .evaluation .SingleResourceEvaluatorBuilder ;
3433import java .io .IOException ;
3534import java .io .InputStream ;
3635import java .io .UncheckedIOException ;
36+ import java .util .List ;
37+ import java .util .Map ;
38+ import java .util .stream .Stream ;
3739import lombok .Value ;
3840import org .apache .spark .sql .Column ;
3941import org .hl7 .fhir .r4 .model .Enumerations .FHIRDefinedType ;
@@ -78,6 +80,21 @@ public class SearchColumnBuilder {
7880 /** Resource path for the bundled R4 search parameters. */
7981 private static final String R4_REGISTRY_RESOURCE = "/fhir/R4/search-parameters.json" ;
8082
83+ /**
84+ * Mappings from complex FHIR types to their string sub-fields.
85+ *
86+ * <p>When a string search parameter expression resolves to one of these complex types, the search
87+ * is expanded to match against each sub-field independently. A match on any sub-field satisfies
88+ * the search criterion.
89+ *
90+ * @see <a href="https://hl7.org/fhir/search.html#string">String Search</a>
91+ */
92+ private static final Map <FHIRDefinedType , List <String >> COMPLEX_TYPE_STRING_SUBFIELDS =
93+ Map .of (
94+ FHIRDefinedType .HUMANNAME , List .of ("family" , "given" , "text" , "prefix" , "suffix" ),
95+ FHIRDefinedType .ADDRESS ,
96+ List .of ("text" , "line" , "city" , "district" , "state" , "postalCode" , "country" ));
97+
8198 /** The FHIR context for resource definitions. */
8299 @ Nonnull FhirContext fhirContext ;
83100
@@ -261,6 +278,11 @@ private Column buildCriterionFilter(
261278 /**
262279 * Builds a filter expression for a single FHIRPath expression within a search criterion.
263280 *
281+ * <p>When the expression resolves to a complex type with known string sub-fields (HumanName,
282+ * Address), the expression is expanded into multiple sub-field expressions that are each
283+ * evaluated independently. The filter results are OR'd together so that a match on any sub-field
284+ * satisfies the criterion.
285+ *
264286 * @param paramType the search parameter type
265287 * @param criterion the search criterion (for modifier and values)
266288 * @param expression the FHIRPath expression to evaluate
@@ -274,14 +296,11 @@ private Column buildExpressionFilter(
274296 @ Nonnull final String expression ,
275297 @ Nonnull final SingleResourceEvaluator evaluator ) {
276298
277- // Parse the FHIRPath expression
299+ // Parse and evaluate the FHIRPath expression to determine its type.
278300 final FhirPath fhirPath = parser .parse (expression );
279-
280- // Evaluate the FHIRPath to extract the value column
281301 final Collection result = evaluator .evaluate (fhirPath );
282- final ColumnRepresentation valueColumn = result .getColumn ();
283302
284- // Get FHIR type from collection - fail if not available
303+ // Get FHIR type from collection - fail if not available.
285304 final FHIRDefinedType fhirType =
286305 result
287306 .getFhirType ()
@@ -290,11 +309,32 @@ private Column buildExpressionFilter(
290309 new InvalidSearchParameterException (
291310 "Cannot determine FHIR type for expression: " + expression ));
292311
293- // Get the appropriate filter for the parameter type, modifier, and FHIR type
294- final SearchFilter filter = getFilterForType (paramType , criterion .getModifier (), fhirType );
312+ // If the expression resolves to a complex type with known string sub-fields, expand into
313+ // sub-field expressions and OR the results together.
314+ final List <String > subFields = COMPLEX_TYPE_STRING_SUBFIELDS .get (fhirType );
315+ if (subFields != null ) {
316+ return subFields .stream ()
317+ .map (subField -> expression + "." + subField )
318+ .flatMap (
319+ subFieldExpr -> {
320+ final FhirPath subFhirPath = parser .parse (subFieldExpr );
321+ final Collection subResult = evaluator .evaluate (subFhirPath );
322+ // Skip sub-fields that do not exist for this resource type.
323+ if (subResult .getFhirType ().isEmpty ()) {
324+ return Stream .empty ();
325+ }
326+ final SearchFilter filter =
327+ getFilterForType (
328+ paramType , criterion .getModifier (), subResult .getFhirType ().get ());
329+ return Stream .of (filter .buildFilter (subResult .getColumn (), criterion .getValues ()));
330+ })
331+ .reduce (Column ::or )
332+ .orElse (lit (false ));
333+ }
295334
296- // Build and return the filter expression
297- return filter .buildFilter (valueColumn , criterion .getValues ());
335+ // Standard path: build filter directly from the evaluated column.
336+ final SearchFilter filter = getFilterForType (paramType , criterion .getModifier (), fhirType );
337+ return filter .buildFilter (result .getColumn (), criterion .getValues ());
298338 }
299339
300340 /**
0 commit comments