132cd2800SGreg Roach<?php 23976b470SGreg Roach 332cd2800SGreg Roach/** 432cd2800SGreg Roach * webtrees: online genealogy 532cd2800SGreg Roach * Copyright (C) 2019 webtrees development team 632cd2800SGreg Roach * This program is free software: you can redistribute it and/or modify 732cd2800SGreg Roach * it under the terms of the GNU General Public License as published by 832cd2800SGreg Roach * the Free Software Foundation, either version 3 of the License, or 932cd2800SGreg Roach * (at your option) any later version. 1032cd2800SGreg Roach * This program is distributed in the hope that it will be useful, 1132cd2800SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 1232cd2800SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1332cd2800SGreg Roach * GNU General Public License for more details. 1432cd2800SGreg Roach * You should have received a copy of the GNU General Public License 1532cd2800SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 1632cd2800SGreg Roach */ 17fcfa147eSGreg Roach 1832cd2800SGreg Roachdeclare(strict_types=1); 1932cd2800SGreg Roach 2032cd2800SGreg Roachnamespace Fisharebest\Webtrees\Services; 2132cd2800SGreg Roach 2232cd2800SGreg Roachuse Closure; 23dfb2cda2SGreg Roachuse Fisharebest\Webtrees\Date; 24d501c45dSGreg Roachuse Fisharebest\Webtrees\Exceptions\HttpServiceUnavailableException; 2532cd2800SGreg Roachuse Fisharebest\Webtrees\Family; 26a7a24840SGreg Roachuse Fisharebest\Webtrees\Gedcom; 2732cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 2852a8ef61SGreg Roachuse Fisharebest\Webtrees\I18N; 2932cd2800SGreg Roachuse Fisharebest\Webtrees\Individual; 3032cd2800SGreg Roachuse Fisharebest\Webtrees\Media; 3132cd2800SGreg Roachuse Fisharebest\Webtrees\Note; 32b68caec6SGreg Roachuse Fisharebest\Webtrees\Place; 3332cd2800SGreg Roachuse Fisharebest\Webtrees\Repository; 342d686e68SGreg Roachuse Fisharebest\Webtrees\Soundex; 3532cd2800SGreg Roachuse Fisharebest\Webtrees\Source; 36b5c8fd7eSGreg Roachuse Fisharebest\Webtrees\Submitter; 3732cd2800SGreg Roachuse Fisharebest\Webtrees\Tree; 3832cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 3932cd2800SGreg Roachuse Illuminate\Database\Query\Builder; 40a7a24840SGreg Roachuse Illuminate\Database\Query\Expression; 4132cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause; 4232cd2800SGreg Roachuse Illuminate\Support\Collection; 4332cd2800SGreg Roachuse stdClass; 443976b470SGreg Roach 45a7a24840SGreg Roachuse function mb_stripos; 4632cd2800SGreg Roach 4732cd2800SGreg Roach/** 4832cd2800SGreg Roach * Search trees for genealogy records. 4932cd2800SGreg Roach */ 5032cd2800SGreg Roachclass SearchService 5132cd2800SGreg Roach{ 52d5ad3db0SGreg Roach /** @var TreeService */ 53d5ad3db0SGreg Roach private $tree_service; 54d5ad3db0SGreg Roach 55d5ad3db0SGreg Roach /** 56d5ad3db0SGreg Roach * SearchService constructor. 57d5ad3db0SGreg Roach * 58d5ad3db0SGreg Roach * @param TreeService $tree_service 59d5ad3db0SGreg Roach */ 60d5ad3db0SGreg Roach public function __construct(TreeService $tree_service) 61d5ad3db0SGreg Roach { 62d5ad3db0SGreg Roach $this->tree_service = $tree_service; 63d5ad3db0SGreg Roach } 64d5ad3db0SGreg Roach 65a7a24840SGreg Roach /** 66a7a24840SGreg Roach * @param Tree[] $trees 67a7a24840SGreg Roach * @param string[] $search 68a7a24840SGreg Roach * 69b5c8fd7eSGreg Roach * @return Collection<Family> 70a7a24840SGreg Roach */ 71a7a24840SGreg Roach public function searchFamilies(array $trees, array $search): Collection 72a7a24840SGreg Roach { 73a7a24840SGreg Roach $query = DB::table('families'); 74a7a24840SGreg Roach 75a7a24840SGreg Roach $this->whereTrees($query, 'f_file', $trees); 76a7a24840SGreg Roach $this->whereSearch($query, 'f_gedcom', $search); 77a7a24840SGreg Roach 78a7a24840SGreg Roach return $query 79a7a24840SGreg Roach ->get() 8052a8ef61SGreg Roach ->each($this->rowLimiter()) 81d5ad3db0SGreg Roach ->map($this->familyRowMapper()) 82a7a24840SGreg Roach ->filter(GedcomRecord::accessFilter()) 837f5fa3c2SGreg Roach ->filter($this->rawGedcomFilter($search)); 84a7a24840SGreg Roach } 85a7a24840SGreg Roach 8632cd2800SGreg Roach /** 8732cd2800SGreg Roach * Search for families by name. 8832cd2800SGreg Roach * 89a7a24840SGreg Roach * @param Tree[] $trees 90a7a24840SGreg Roach * @param string[] $search 9132cd2800SGreg Roach * @param int $offset 9232cd2800SGreg Roach * @param int $limit 9332cd2800SGreg Roach * 94b5c8fd7eSGreg Roach * @return Collection<Family> 9532cd2800SGreg Roach */ 96a7a24840SGreg Roach public function searchFamilyNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 9732cd2800SGreg Roach { 9832cd2800SGreg Roach $query = DB::table('families') 99ac499332SGreg Roach ->leftJoin('name AS husb_name', static function (JoinClause $join): void { 10032cd2800SGreg Roach $join 10132cd2800SGreg Roach ->on('husb_name.n_file', '=', 'families.f_file') 102ac499332SGreg Roach ->on('husb_name.n_id', '=', 'families.f_husb') 103ac499332SGreg Roach ->where('husb_name.n_type', '<>', '_MARNM'); 10432cd2800SGreg Roach }) 105ac499332SGreg Roach ->leftJoin('name AS wife_name', static function (JoinClause $join): void { 10632cd2800SGreg Roach $join 10732cd2800SGreg Roach ->on('wife_name.n_file', '=', 'families.f_file') 108ac499332SGreg Roach ->on('wife_name.n_id', '=', 'families.f_wife') 109ac499332SGreg Roach ->where('wife_name.n_type', '<>', '_MARNM'); 110ac499332SGreg Roach }); 111a7a24840SGreg Roach 112a7a24840SGreg Roach $prefix = DB::connection()->getTablePrefix(); 113a69f5655SGreg Roach $field = new Expression('COALESCE(' . $prefix . "husb_name.n_full, '') || COALESCE(" . $prefix . "wife_name.n_full, '')"); 114a7a24840SGreg Roach 115a7a24840SGreg Roach $this->whereTrees($query, 'f_file', $trees); 116a7a24840SGreg Roach $this->whereSearch($query, $field, $search); 117a7a24840SGreg Roach 118a7a24840SGreg Roach $query 11932cd2800SGreg Roach ->orderBy('husb_name.n_sort') 12032cd2800SGreg Roach ->orderBy('wife_name.n_sort') 121c0804649SGreg Roach ->select(['families.*', 'husb_name.n_sort', 'wife_name.n_sort']) 12232cd2800SGreg Roach ->distinct(); 12332cd2800SGreg Roach 124d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->familyRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 125a7a24840SGreg Roach } 126a7a24840SGreg Roach 127a7a24840SGreg Roach /** 128*dbe53437SGreg Roach * @param Place $place 129*dbe53437SGreg Roach * 130*dbe53437SGreg Roach * @return Collection<Family> 131*dbe53437SGreg Roach */ 132*dbe53437SGreg Roach public function searchFamiliesInPlace(Place $place): Collection 133*dbe53437SGreg Roach { 134*dbe53437SGreg Roach return DB::table('families') 135*dbe53437SGreg Roach ->join('placelinks', static function (JoinClause $query) { 136*dbe53437SGreg Roach $query 137*dbe53437SGreg Roach ->on('families.f_file', '=', 'placelinks.pl_file') 138*dbe53437SGreg Roach ->on('families.f_id', '=', 'placelinks.pl_gid'); 139*dbe53437SGreg Roach }) 140*dbe53437SGreg Roach ->where('f_file', '=', $place->tree()->id()) 141*dbe53437SGreg Roach ->where('pl_p_id', '=', $place->id()) 142*dbe53437SGreg Roach ->select(['families.*']) 143*dbe53437SGreg Roach ->get() 144*dbe53437SGreg Roach ->each($this->rowLimiter()) 145*dbe53437SGreg Roach ->map($this->familyRowMapper()) 146*dbe53437SGreg Roach ->filter(GedcomRecord::accessFilter()); 147*dbe53437SGreg Roach } 148*dbe53437SGreg Roach 149*dbe53437SGreg Roach /** 150a7a24840SGreg Roach * @param Tree[] $trees 151a7a24840SGreg Roach * @param string[] $search 152a7a24840SGreg Roach * 153b5c8fd7eSGreg Roach * @return Collection<Individual> 154a7a24840SGreg Roach */ 155a7a24840SGreg Roach public function searchIndividuals(array $trees, array $search): Collection 156a7a24840SGreg Roach { 157a7a24840SGreg Roach $query = DB::table('individuals'); 158a7a24840SGreg Roach 159a7a24840SGreg Roach $this->whereTrees($query, 'i_file', $trees); 160a7a24840SGreg Roach $this->whereSearch($query, 'i_gedcom', $search); 161a7a24840SGreg Roach 162a7a24840SGreg Roach return $query 163a7a24840SGreg Roach ->get() 16452a8ef61SGreg Roach ->each($this->rowLimiter()) 165d5ad3db0SGreg Roach ->map($this->individualRowMapper()) 166a7a24840SGreg Roach ->filter(GedcomRecord::accessFilter()) 1677f5fa3c2SGreg Roach ->filter($this->rawGedcomFilter($search)); 16832cd2800SGreg Roach } 16932cd2800SGreg Roach 17032cd2800SGreg Roach /** 17132cd2800SGreg Roach * Search for individuals by name. 17232cd2800SGreg Roach * 173a7a24840SGreg Roach * @param Tree[] $trees 174a7a24840SGreg Roach * @param string[] $search 17532cd2800SGreg Roach * @param int $offset 17632cd2800SGreg Roach * @param int $limit 17732cd2800SGreg Roach * 178b5c8fd7eSGreg Roach * @return Collection<Individual> 17932cd2800SGreg Roach */ 180a7a24840SGreg Roach public function searchIndividualNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 18132cd2800SGreg Roach { 18232cd2800SGreg Roach $query = DB::table('individuals') 1830b5fd0a6SGreg Roach ->join('name', static function (JoinClause $join): void { 18432cd2800SGreg Roach $join 18532cd2800SGreg Roach ->on('name.n_file', '=', 'individuals.i_file') 186a7a24840SGreg Roach ->on('name.n_id', '=', 'individuals.i_id'); 18732cd2800SGreg Roach }) 188e84cf2deSGreg Roach ->orderBy('n_sort') 189d78d61f7SGreg Roach ->distinct() 190685de081SGreg Roach ->select(['individuals.*', 'n_sort']); 19132cd2800SGreg Roach 192a7a24840SGreg Roach $this->whereTrees($query, 'i_file', $trees); 193a7a24840SGreg Roach $this->whereSearch($query, 'n_full', $search); 194a7a24840SGreg Roach 195d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->individualRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 19632cd2800SGreg Roach } 19732cd2800SGreg Roach 19832cd2800SGreg Roach /** 199*dbe53437SGreg Roach * @param Place $place 200*dbe53437SGreg Roach * 201*dbe53437SGreg Roach * @return Collection<Individual> 202*dbe53437SGreg Roach */ 203*dbe53437SGreg Roach public function searchIndividualsInPlace(Place $place): Collection 204*dbe53437SGreg Roach { 205*dbe53437SGreg Roach return DB::table('individuals') 206*dbe53437SGreg Roach ->join('placelinks', static function (JoinClause $join) { 207*dbe53437SGreg Roach $join 208*dbe53437SGreg Roach ->on('i_file', '=', 'pl_file') 209*dbe53437SGreg Roach ->on('i_id', '=', 'pl_gid'); 210*dbe53437SGreg Roach }) 211*dbe53437SGreg Roach ->where('i_file', '=', $place->tree()->id()) 212*dbe53437SGreg Roach ->where('pl_p_id', '=', $place->id()) 213*dbe53437SGreg Roach ->select(['individuals.*']) 214*dbe53437SGreg Roach ->get() 215*dbe53437SGreg Roach ->each($this->rowLimiter()) 216*dbe53437SGreg Roach ->map($this->individualRowMapper()) 217*dbe53437SGreg Roach ->filter(GedcomRecord::accessFilter()); 218*dbe53437SGreg Roach } 219*dbe53437SGreg Roach 220*dbe53437SGreg Roach /** 22132cd2800SGreg Roach * Search for media objects. 22232cd2800SGreg Roach * 223a7a24840SGreg Roach * @param Tree[] $trees 224a7a24840SGreg Roach * @param string[] $search 22532cd2800SGreg Roach * @param int $offset 22632cd2800SGreg Roach * @param int $limit 22732cd2800SGreg Roach * 228b5c8fd7eSGreg Roach * @return Collection<Media> 22932cd2800SGreg Roach */ 230a7a24840SGreg Roach public function searchMedia(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 23132cd2800SGreg Roach { 232a7a24840SGreg Roach $query = DB::table('media'); 23332cd2800SGreg Roach 234a7a24840SGreg Roach $this->whereTrees($query, 'media.m_file', $trees); 235a7a24840SGreg Roach $this->whereSearch($query, 'm_gedcom', $search); 236a7a24840SGreg Roach 237d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->mediaRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 23832cd2800SGreg Roach } 23932cd2800SGreg Roach 24032cd2800SGreg Roach /** 24132cd2800SGreg Roach * Search for notes. 24232cd2800SGreg Roach * 243a7a24840SGreg Roach * @param Tree[] $trees 244a7a24840SGreg Roach * @param string[] $search 24532cd2800SGreg Roach * @param int $offset 24632cd2800SGreg Roach * @param int $limit 24732cd2800SGreg Roach * 248b5c8fd7eSGreg Roach * @return Collection<Note> 24932cd2800SGreg Roach */ 250a7a24840SGreg Roach public function searchNotes(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 25132cd2800SGreg Roach { 25232cd2800SGreg Roach $query = DB::table('other') 253a7a24840SGreg Roach ->where('o_type', '=', 'NOTE'); 25432cd2800SGreg Roach 255a7a24840SGreg Roach $this->whereTrees($query, 'o_file', $trees); 256a7a24840SGreg Roach $this->whereSearch($query, 'o_gedcom', $search); 257a7a24840SGreg Roach 258d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->noteRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 25932cd2800SGreg Roach } 26032cd2800SGreg Roach 26132cd2800SGreg Roach /** 26232cd2800SGreg Roach * Search for repositories. 26332cd2800SGreg Roach * 264a7a24840SGreg Roach * @param Tree[] $trees 265a7a24840SGreg Roach * @param string[] $search 26632cd2800SGreg Roach * @param int $offset 26732cd2800SGreg Roach * @param int $limit 26832cd2800SGreg Roach * 269b5c8fd7eSGreg Roach * @return Collection<Repository> 27032cd2800SGreg Roach */ 271a7a24840SGreg Roach public function searchRepositories(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 27232cd2800SGreg Roach { 27332cd2800SGreg Roach $query = DB::table('other') 274a7a24840SGreg Roach ->where('o_type', '=', 'REPO'); 27532cd2800SGreg Roach 276a7a24840SGreg Roach $this->whereTrees($query, 'o_file', $trees); 277a7a24840SGreg Roach $this->whereSearch($query, 'o_gedcom', $search); 278a7a24840SGreg Roach 279d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->repositoryRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 28032cd2800SGreg Roach } 28132cd2800SGreg Roach 28232cd2800SGreg Roach /** 283a7a24840SGreg Roach * Search for sources. 28432cd2800SGreg Roach * 285a7a24840SGreg Roach * @param Tree[] $trees 286a7a24840SGreg Roach * @param string[] $search 28732cd2800SGreg Roach * @param int $offset 28832cd2800SGreg Roach * @param int $limit 28932cd2800SGreg Roach * 290b5c8fd7eSGreg Roach * @return Collection<Source> 29132cd2800SGreg Roach */ 292a7a24840SGreg Roach public function searchSources(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 293a7a24840SGreg Roach { 294a7a24840SGreg Roach $query = DB::table('sources'); 295a7a24840SGreg Roach 296a7a24840SGreg Roach $this->whereTrees($query, 's_file', $trees); 297a7a24840SGreg Roach $this->whereSearch($query, 's_gedcom', $search); 298a7a24840SGreg Roach 299d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 300a7a24840SGreg Roach } 301a7a24840SGreg Roach 302a7a24840SGreg Roach /** 303a7a24840SGreg Roach * Search for sources by name. 304a7a24840SGreg Roach * 305a7a24840SGreg Roach * @param Tree[] $trees 306a7a24840SGreg Roach * @param string[] $search 307a7a24840SGreg Roach * @param int $offset 308a7a24840SGreg Roach * @param int $limit 309a7a24840SGreg Roach * 310b5c8fd7eSGreg Roach * @return Collection<Source> 311a7a24840SGreg Roach */ 312a7a24840SGreg Roach public function searchSourcesByName(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 31332cd2800SGreg Roach { 31432cd2800SGreg Roach $query = DB::table('sources') 315c0804649SGreg Roach ->orderBy('s_name'); 31632cd2800SGreg Roach 317a7a24840SGreg Roach $this->whereTrees($query, 's_file', $trees); 318a7a24840SGreg Roach $this->whereSearch($query, 's_name', $search); 319a7a24840SGreg Roach 320d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->sourceRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 32132cd2800SGreg Roach } 32232cd2800SGreg Roach 32332cd2800SGreg Roach /** 32432cd2800SGreg Roach * Search for submitters. 32532cd2800SGreg Roach * 326a7a24840SGreg Roach * @param Tree[] $trees 327a7a24840SGreg Roach * @param string[] $search 32832cd2800SGreg Roach * @param int $offset 32932cd2800SGreg Roach * @param int $limit 33032cd2800SGreg Roach * 331b5c8fd7eSGreg Roach * @return Collection<Submitter> 33232cd2800SGreg Roach */ 333a7a24840SGreg Roach public function searchSubmitters(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 33432cd2800SGreg Roach { 33532cd2800SGreg Roach $query = DB::table('other') 336a7a24840SGreg Roach ->where('o_type', '=', 'SUBM'); 33732cd2800SGreg Roach 338a7a24840SGreg Roach $this->whereTrees($query, 'o_file', $trees); 339a7a24840SGreg Roach $this->whereSearch($query, 'o_gedcom', $search); 340a7a24840SGreg Roach 341d5ad3db0SGreg Roach return $this->paginateQuery($query, $this->submitterRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 34232cd2800SGreg Roach } 34332cd2800SGreg Roach 34432cd2800SGreg Roach /** 345b68caec6SGreg Roach * Search for places. 346b68caec6SGreg Roach * 347b68caec6SGreg Roach * @param Tree $tree 348b68caec6SGreg Roach * @param string $search 349b68caec6SGreg Roach * @param int $offset 350b68caec6SGreg Roach * @param int $limit 351b68caec6SGreg Roach * 352b5c8fd7eSGreg Roach * @return Collection<Place> 353b68caec6SGreg Roach */ 354b68caec6SGreg Roach public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection 355b68caec6SGreg Roach { 356b68caec6SGreg Roach $query = DB::table('places AS p0') 357b68caec6SGreg Roach ->where('p0.p_file', '=', $tree->id()) 358b68caec6SGreg Roach ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id') 359b68caec6SGreg Roach ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id') 360b68caec6SGreg Roach ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id') 361b68caec6SGreg Roach ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id') 362b68caec6SGreg Roach ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id') 363b68caec6SGreg Roach ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id') 364b68caec6SGreg Roach ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id') 365b68caec6SGreg Roach ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id') 366b68caec6SGreg Roach ->orderBy('p0.p_place') 367b68caec6SGreg Roach ->orderBy('p1.p_place') 368b68caec6SGreg Roach ->orderBy('p2.p_place') 369b68caec6SGreg Roach ->orderBy('p3.p_place') 370b68caec6SGreg Roach ->orderBy('p4.p_place') 371b68caec6SGreg Roach ->orderBy('p5.p_place') 372b68caec6SGreg Roach ->orderBy('p6.p_place') 373b68caec6SGreg Roach ->orderBy('p7.p_place') 374b68caec6SGreg Roach ->orderBy('p8.p_place') 375b68caec6SGreg Roach ->select([ 376b68caec6SGreg Roach 'p0.p_place AS place0', 377b68caec6SGreg Roach 'p1.p_place AS place1', 378b68caec6SGreg Roach 'p2.p_place AS place2', 379b68caec6SGreg Roach 'p3.p_place AS place3', 380b68caec6SGreg Roach 'p4.p_place AS place4', 381b68caec6SGreg Roach 'p5.p_place AS place5', 382b68caec6SGreg Roach 'p6.p_place AS place6', 383b68caec6SGreg Roach 'p7.p_place AS place7', 384b68caec6SGreg Roach 'p8.p_place AS place8', 385b68caec6SGreg Roach ]); 386b68caec6SGreg Roach 387b68caec6SGreg Roach // Filter each level of the hierarchy. 388b68caec6SGreg Roach foreach (explode(',', $search, 9) as $level => $string) { 389b68caec6SGreg Roach $query->whereContains('p' . $level . '.p_place', $string); 390b68caec6SGreg Roach } 391b68caec6SGreg Roach 3926c2179e2SGreg Roach $row_mapper = static function (stdClass $row) use ($tree): Place { 393b68caec6SGreg Roach $place = implode(', ', array_filter((array) $row)); 394b68caec6SGreg Roach 395b68caec6SGreg Roach return new Place($place, $tree); 396b68caec6SGreg Roach }; 397b68caec6SGreg Roach 3986c2179e2SGreg Roach $filter = static function (): bool { 399a7a24840SGreg Roach return true; 400a7a24840SGreg Roach }; 401b68caec6SGreg Roach 402a7a24840SGreg Roach return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit); 403a7a24840SGreg Roach } 404b68caec6SGreg Roach 405b68caec6SGreg Roach /** 406a5dbc5b2SGreg Roach * @param Tree[] $trees 407dfb2cda2SGreg Roach * @param string[] $fields 408dfb2cda2SGreg Roach * @param string[] $modifiers 409dfb2cda2SGreg Roach * 410b5c8fd7eSGreg Roach * @return Collection<Individual> 411dfb2cda2SGreg Roach */ 412dfb2cda2SGreg Roach public function searchIndividualsAdvanced(array $trees, array $fields, array $modifiers): Collection 413dfb2cda2SGreg Roach { 414dfb2cda2SGreg Roach $fields = array_filter($fields); 415dfb2cda2SGreg Roach 416dfb2cda2SGreg Roach $query = DB::table('individuals') 417dfb2cda2SGreg Roach ->select(['individuals.*']) 418dfb2cda2SGreg Roach ->distinct(); 419dfb2cda2SGreg Roach 420dfb2cda2SGreg Roach $this->whereTrees($query, 'i_file', $trees); 421dfb2cda2SGreg Roach 422dfb2cda2SGreg Roach // Join the following tables 423dfb2cda2SGreg Roach $father_name = false; 424dfb2cda2SGreg Roach $mother_name = false; 425dfb2cda2SGreg Roach $spouse_family = false; 426dfb2cda2SGreg Roach $indi_name = false; 427dfb2cda2SGreg Roach $indi_date = false; 428dfb2cda2SGreg Roach $fam_date = false; 429dfb2cda2SGreg Roach $indi_plac = false; 430dfb2cda2SGreg Roach $fam_plac = false; 431dfb2cda2SGreg Roach 432dfb2cda2SGreg Roach foreach ($fields as $field_name => $field_value) { 433dfb2cda2SGreg Roach if ($field_value !== '') { 434dfb2cda2SGreg Roach if (substr($field_name, 0, 14) === 'FAMC:HUSB:NAME') { 435dfb2cda2SGreg Roach $father_name = true; 436dfb2cda2SGreg Roach } elseif (substr($field_name, 0, 14) === 'FAMC:WIFE:NAME') { 437dfb2cda2SGreg Roach $mother_name = true; 438dfb2cda2SGreg Roach } elseif (substr($field_name, 0, 4) === 'NAME') { 439dfb2cda2SGreg Roach $indi_name = true; 440dfb2cda2SGreg Roach } elseif (strpos($field_name, ':DATE') !== false) { 441dfb2cda2SGreg Roach if (substr($field_name, 0, 4) === 'FAMS') { 442dfb2cda2SGreg Roach $fam_date = true; 443dfb2cda2SGreg Roach $spouse_family = true; 444dfb2cda2SGreg Roach } else { 445dfb2cda2SGreg Roach $indi_date = true; 446dfb2cda2SGreg Roach } 447dfb2cda2SGreg Roach } elseif (strpos($field_name, ':PLAC') !== false) { 448dfb2cda2SGreg Roach if (substr($field_name, 0, 4) === 'FAMS') { 449dfb2cda2SGreg Roach $fam_plac = true; 450dfb2cda2SGreg Roach $spouse_family = true; 451dfb2cda2SGreg Roach } else { 452dfb2cda2SGreg Roach $indi_plac = true; 453dfb2cda2SGreg Roach } 454dfb2cda2SGreg Roach } elseif ($field_name === 'FAMS:NOTE') { 455dfb2cda2SGreg Roach $spouse_family = true; 456dfb2cda2SGreg Roach } 457dfb2cda2SGreg Roach } 458dfb2cda2SGreg Roach } 459dfb2cda2SGreg Roach 460dfb2cda2SGreg Roach if ($father_name || $mother_name) { 4610b5fd0a6SGreg Roach $query->join('link AS l1', static function (JoinClause $join): void { 462dfb2cda2SGreg Roach $join 463dfb2cda2SGreg Roach ->on('l1.l_file', '=', 'individuals.i_file') 464dfb2cda2SGreg Roach ->on('l1.l_from', '=', 'individuals.i_id') 465dfb2cda2SGreg Roach ->where('l1.l_type', '=', 'FAMC'); 466dfb2cda2SGreg Roach }); 467dfb2cda2SGreg Roach 468dfb2cda2SGreg Roach if ($father_name) { 4690b5fd0a6SGreg Roach $query->join('link AS l2', static function (JoinClause $join): void { 470dfb2cda2SGreg Roach $join 471dfb2cda2SGreg Roach ->on('l2.l_file', '=', 'l1.l_file') 472dfb2cda2SGreg Roach ->on('l2.l_from', '=', 'l1.l_to') 473dfb2cda2SGreg Roach ->where('l2.l_type', '=', 'HUSB'); 474dfb2cda2SGreg Roach }); 4750b5fd0a6SGreg Roach $query->join('name AS father_name', static function (JoinClause $join): void { 476dfb2cda2SGreg Roach $join 477dfb2cda2SGreg Roach ->on('father_name.n_file', '=', 'l2.l_file') 478dfb2cda2SGreg Roach ->on('father_name.n_id', '=', 'l2.l_to'); 479dfb2cda2SGreg Roach }); 480dfb2cda2SGreg Roach } 481dfb2cda2SGreg Roach 482dfb2cda2SGreg Roach if ($mother_name) { 4830b5fd0a6SGreg Roach $query->join('link AS l3', static function (JoinClause $join): void { 484dfb2cda2SGreg Roach $join 485dfb2cda2SGreg Roach ->on('l3.l_file', '=', 'l1.l_file') 486dfb2cda2SGreg Roach ->on('l3.l_from', '=', 'l1.l_to') 487dfb2cda2SGreg Roach ->where('l3.l_type', '=', 'WIFE'); 488dfb2cda2SGreg Roach }); 4890b5fd0a6SGreg Roach $query->join('name AS mother_name', static function (JoinClause $join): void { 490dfb2cda2SGreg Roach $join 491dfb2cda2SGreg Roach ->on('mother_name.n_file', '=', 'l3.l_file') 492dfb2cda2SGreg Roach ->on('mother_name.n_id', '=', 'l3.l_to'); 493dfb2cda2SGreg Roach }); 494dfb2cda2SGreg Roach } 495dfb2cda2SGreg Roach } 496dfb2cda2SGreg Roach 497dfb2cda2SGreg Roach if ($spouse_family) { 4980b5fd0a6SGreg Roach $query->join('link AS l4', static function (JoinClause $join): void { 499dfb2cda2SGreg Roach $join 500dfb2cda2SGreg Roach ->on('l4.l_file', '=', 'individuals.i_file') 501dfb2cda2SGreg Roach ->on('l4.l_from', '=', 'individuals.i_id') 502dfb2cda2SGreg Roach ->where('l4.l_type', '=', 'FAMS'); 503dfb2cda2SGreg Roach }); 5040b5fd0a6SGreg Roach $query->join('families AS spouse_families', static function (JoinClause $join): void { 505dfb2cda2SGreg Roach $join 506dfb2cda2SGreg Roach ->on('spouse_families.f_file', '=', 'l4.l_file') 507dfb2cda2SGreg Roach ->on('spouse_families.f_id', '=', 'l4.l_to'); 508dfb2cda2SGreg Roach }); 509dfb2cda2SGreg Roach } 510dfb2cda2SGreg Roach 511dfb2cda2SGreg Roach if ($indi_name) { 5120b5fd0a6SGreg Roach $query->join('name AS individual_name', static function (JoinClause $join): void { 513dfb2cda2SGreg Roach $join 514dfb2cda2SGreg Roach ->on('individual_name.n_file', '=', 'individuals.i_file') 515dfb2cda2SGreg Roach ->on('individual_name.n_id', '=', 'individuals.i_id'); 516dfb2cda2SGreg Roach }); 517dfb2cda2SGreg Roach } 518dfb2cda2SGreg Roach 519dfb2cda2SGreg Roach if ($indi_date) { 5200b5fd0a6SGreg Roach $query->join('dates AS individual_dates', static function (JoinClause $join): void { 521dfb2cda2SGreg Roach $join 522dfb2cda2SGreg Roach ->on('individual_dates.d_file', '=', 'individuals.i_file') 523dfb2cda2SGreg Roach ->on('individual_dates.d_gid', '=', 'individuals.i_id'); 524dfb2cda2SGreg Roach }); 525dfb2cda2SGreg Roach } 526dfb2cda2SGreg Roach 527dfb2cda2SGreg Roach if ($fam_date) { 5280b5fd0a6SGreg Roach $query->join('dates AS family_dates', static function (JoinClause $join): void { 529dfb2cda2SGreg Roach $join 530dfb2cda2SGreg Roach ->on('family_dates.d_file', '=', 'spouse_families.f_file') 531dfb2cda2SGreg Roach ->on('family_dates.d_gid', '=', 'spouse_families.f_id'); 532dfb2cda2SGreg Roach }); 533dfb2cda2SGreg Roach } 534dfb2cda2SGreg Roach 535dfb2cda2SGreg Roach if ($indi_plac) { 5360b5fd0a6SGreg Roach $query->join('placelinks AS individual_placelinks', static function (JoinClause $join): void { 537dfb2cda2SGreg Roach $join 538dfb2cda2SGreg Roach ->on('individual_placelinks.pl_file', '=', 'individuals.i_file') 539dfb2cda2SGreg Roach ->on('individual_placelinks.pl_gid', '=', 'individuals.i_id'); 540dfb2cda2SGreg Roach }); 5410b5fd0a6SGreg Roach $query->join('places AS individual_places', static function (JoinClause $join): void { 542dfb2cda2SGreg Roach $join 543dfb2cda2SGreg Roach ->on('individual_places.p_file', '=', 'individual_placelinks.pl_file') 544dfb2cda2SGreg Roach ->on('individual_places.p_id', '=', 'individual_placelinks.pl_p_id'); 545dfb2cda2SGreg Roach }); 546dfb2cda2SGreg Roach } 547dfb2cda2SGreg Roach 548dfb2cda2SGreg Roach if ($fam_plac) { 5490b5fd0a6SGreg Roach $query->join('placelinks AS familyl_placelinks', static function (JoinClause $join): void { 550dfb2cda2SGreg Roach $join 551dfb2cda2SGreg Roach ->on('familyl_placelinks.pl_file', '=', 'individuals.i_file') 552dfb2cda2SGreg Roach ->on('familyl_placelinks.pl_gid', '=', 'individuals.i_id'); 553dfb2cda2SGreg Roach }); 5540b5fd0a6SGreg Roach $query->join('places AS family_places', static function (JoinClause $join): void { 555dfb2cda2SGreg Roach $join 556dfb2cda2SGreg Roach ->on('family_places.p_file', '=', 'familyl_placelinks.pl_file') 557dfb2cda2SGreg Roach ->on('family_places.p_id', '=', 'familyl_placelinks.pl_p_id'); 558dfb2cda2SGreg Roach }); 559dfb2cda2SGreg Roach } 560dfb2cda2SGreg Roach 561dfb2cda2SGreg Roach foreach ($fields as $field_name => $field_value) { 5623cfcc809SGreg Roach $parts = explode(':', $field_name . '::::'); 563dfb2cda2SGreg Roach if ($parts[0] === 'NAME') { 564dfb2cda2SGreg Roach // NAME:* 565dfb2cda2SGreg Roach switch ($parts[1]) { 566dfb2cda2SGreg Roach case 'GIVN': 567dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 568dfb2cda2SGreg Roach case 'EXACT': 569dfb2cda2SGreg Roach $query->where('individual_name.n_givn', '=', $field_value); 570dfb2cda2SGreg Roach break; 571dfb2cda2SGreg Roach case 'BEGINS': 572dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', $field_value . '%'); 573dfb2cda2SGreg Roach break; 574dfb2cda2SGreg Roach case 'CONTAINS': 575dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%'); 576dfb2cda2SGreg Roach break; 577dfb2cda2SGreg Roach case 'SDX_STD': 578dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 579dfb2cda2SGreg Roach if ($sdx !== '') { 580dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_givn_std', $sdx); 581dfb2cda2SGreg Roach } else { 582dfb2cda2SGreg Roach // No phonetic content? Use a substring match 583dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%'); 584dfb2cda2SGreg Roach } 585dfb2cda2SGreg Roach break; 586dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 587dfb2cda2SGreg Roach case 'SDX_DM': 588dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 589dfb2cda2SGreg Roach if ($sdx !== '') { 590dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_givn_dm', $sdx); 591dfb2cda2SGreg Roach } else { 592dfb2cda2SGreg Roach // No phonetic content? Use a substring match 593dfb2cda2SGreg Roach $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%'); 594dfb2cda2SGreg Roach } 595dfb2cda2SGreg Roach break; 596dfb2cda2SGreg Roach } 597dfb2cda2SGreg Roach break; 598dfb2cda2SGreg Roach case 'SURN': 599dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 600dfb2cda2SGreg Roach case 'EXACT': 601dfb2cda2SGreg Roach $query->where('individual_name.n_surn', '=', $field_value); 602dfb2cda2SGreg Roach break; 603dfb2cda2SGreg Roach case 'BEGINS': 604dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', $field_value . '%'); 605dfb2cda2SGreg Roach break; 606dfb2cda2SGreg Roach case 'CONTAINS': 607dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%'); 608dfb2cda2SGreg Roach break; 609dfb2cda2SGreg Roach case 'SDX_STD': 610dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 611dfb2cda2SGreg Roach if ($sdx !== '') { 612dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_surn_std', $sdx); 613dfb2cda2SGreg Roach } else { 614dfb2cda2SGreg Roach // No phonetic content? Use a substring match 615dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%'); 616dfb2cda2SGreg Roach } 617dfb2cda2SGreg Roach break; 618dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 619dfb2cda2SGreg Roach case 'SDX_DM': 620dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 621dfb2cda2SGreg Roach if ($sdx !== '') { 622dfb2cda2SGreg Roach $this->wherePhonetic($query, 'individual_name.n_soundex_surn_dm', $sdx); 623dfb2cda2SGreg Roach } else { 624dfb2cda2SGreg Roach // No phonetic content? Use a substring match 625dfb2cda2SGreg Roach $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%'); 626dfb2cda2SGreg Roach } 627dfb2cda2SGreg Roach break; 628dfb2cda2SGreg Roach } 629dfb2cda2SGreg Roach break; 630dfb2cda2SGreg Roach case 'NICK': 631dfb2cda2SGreg Roach case '_MARNM': 632dfb2cda2SGreg Roach case '_HEB': 633dfb2cda2SGreg Roach case '_AKA': 634dfb2cda2SGreg Roach $query 635dfb2cda2SGreg Roach ->where('individual_name', '=', $parts[1]) 636dfb2cda2SGreg Roach ->where('individual_name', 'LIKE', '%' . $field_value . '%'); 637dfb2cda2SGreg Roach break; 638dfb2cda2SGreg Roach } 639dfb2cda2SGreg Roach unset($fields[$field_name]); 640dfb2cda2SGreg Roach } elseif ($parts[1] === 'DATE') { 641dfb2cda2SGreg Roach // *:DATE 642dfb2cda2SGreg Roach $date = new Date($field_value); 643dfb2cda2SGreg Roach if ($date->isOK()) { 644dfb2cda2SGreg Roach $delta = 365 * ($modifiers[$field_name] ?? 0); 645dfb2cda2SGreg Roach $query 646dfb2cda2SGreg Roach ->where('individual_dates.d_fact', '=', $parts[0]) 647dfb2cda2SGreg Roach ->where('individual_dates.d_julianday1', '>=', $date->minimumJulianDay() - $delta) 648dfb2cda2SGreg Roach ->where('individual_dates.d_julianday2', '<=', $date->minimumJulianDay() + $delta); 649dfb2cda2SGreg Roach } 650dfb2cda2SGreg Roach unset($fields[$field_name]); 651dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMS' && $parts[2] === 'DATE') { 652dfb2cda2SGreg Roach // FAMS:*:DATE 653dfb2cda2SGreg Roach $date = new Date($field_value); 654dfb2cda2SGreg Roach if ($date->isOK()) { 655dfb2cda2SGreg Roach $delta = 365 * $modifiers[$field_name]; 656dfb2cda2SGreg Roach $query 657dfb2cda2SGreg Roach ->where('family_dates.d_fact', '=', $parts[1]) 658dfb2cda2SGreg Roach ->where('family_dates.d_julianday1', '>=', $date->minimumJulianDay() - $delta) 659dfb2cda2SGreg Roach ->where('family_dates.d_julianday2', '<=', $date->minimumJulianDay() + $delta); 660dfb2cda2SGreg Roach } 661dfb2cda2SGreg Roach unset($fields[$field_name]); 662dfb2cda2SGreg Roach } elseif ($parts[1] === 'PLAC') { 663dfb2cda2SGreg Roach // *:PLAC 664dfb2cda2SGreg Roach // SQL can only link a place to a person/family, not to an event. 665dfb2cda2SGreg Roach $query->where('individual_places.p_place', 'LIKE', '%' . $field_value . '%'); 666dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') { 667dfb2cda2SGreg Roach // FAMS:*:PLAC 668dfb2cda2SGreg Roach // SQL can only link a place to a person/family, not to an event. 669dfb2cda2SGreg Roach $query->where('family_places.p_place', 'LIKE', '%' . $field_value . '%'); 670dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMC' && $parts[2] === 'NAME') { 671dfb2cda2SGreg Roach $table = $parts[1] === 'HUSB' ? 'father_name' : 'mother_name'; 672dfb2cda2SGreg Roach // NAME:* 673dfb2cda2SGreg Roach switch ($parts[3]) { 674dfb2cda2SGreg Roach case 'GIVN': 675dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 676dfb2cda2SGreg Roach case 'EXACT': 677dfb2cda2SGreg Roach $query->where($table . '.n_givn', '=', $field_value); 678dfb2cda2SGreg Roach break; 679dfb2cda2SGreg Roach case 'BEGINS': 680dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', $field_value . '%'); 681dfb2cda2SGreg Roach break; 682dfb2cda2SGreg Roach case 'CONTAINS': 683dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%'); 684dfb2cda2SGreg Roach break; 685dfb2cda2SGreg Roach case 'SDX_STD': 686dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 687dfb2cda2SGreg Roach if ($sdx !== '') { 688dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_givn_std', $sdx); 689dfb2cda2SGreg Roach } else { 690dfb2cda2SGreg Roach // No phonetic content? Use a substring match 691dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%'); 692dfb2cda2SGreg Roach } 693dfb2cda2SGreg Roach break; 694dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 695dfb2cda2SGreg Roach case 'SDX_DM': 696dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 697dfb2cda2SGreg Roach if ($sdx !== '') { 698dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_givn_dm', $sdx); 699dfb2cda2SGreg Roach } else { 700dfb2cda2SGreg Roach // No phonetic content? Use a substring match 701dfb2cda2SGreg Roach $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%'); 702dfb2cda2SGreg Roach } 703dfb2cda2SGreg Roach break; 704dfb2cda2SGreg Roach } 705dfb2cda2SGreg Roach break; 706dfb2cda2SGreg Roach case 'SURN': 707dfb2cda2SGreg Roach switch ($modifiers[$field_name]) { 708dfb2cda2SGreg Roach case 'EXACT': 709dfb2cda2SGreg Roach $query->where($table . '.n_surn', '=', $field_value); 710dfb2cda2SGreg Roach break; 711dfb2cda2SGreg Roach case 'BEGINS': 712dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', $field_value . '%'); 713dfb2cda2SGreg Roach break; 714dfb2cda2SGreg Roach case 'CONTAINS': 715dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%'); 716dfb2cda2SGreg Roach break; 717dfb2cda2SGreg Roach case 'SDX_STD': 718dfb2cda2SGreg Roach $sdx = Soundex::russell($field_value); 719dfb2cda2SGreg Roach if ($sdx !== '') { 720dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_surn_std', $sdx); 721dfb2cda2SGreg Roach } else { 722dfb2cda2SGreg Roach // No phonetic content? Use a substring match 723dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%'); 724dfb2cda2SGreg Roach } 725dfb2cda2SGreg Roach break; 726dfb2cda2SGreg Roach case 'SDX': // SDX uses DM by default. 727dfb2cda2SGreg Roach case 'SDX_DM': 728dfb2cda2SGreg Roach $sdx = Soundex::daitchMokotoff($field_value); 729dfb2cda2SGreg Roach if ($sdx !== '') { 730dfb2cda2SGreg Roach $this->wherePhonetic($query, $table . '.n_soundex_surn_dm', $sdx); 731dfb2cda2SGreg Roach } else { 732dfb2cda2SGreg Roach // No phonetic content? Use a substring match 733dfb2cda2SGreg Roach $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%'); 734dfb2cda2SGreg Roach } 735dfb2cda2SGreg Roach break; 736dfb2cda2SGreg Roach } 737dfb2cda2SGreg Roach break; 738dfb2cda2SGreg Roach } 739dfb2cda2SGreg Roach unset($fields[$field_name]); 740dfb2cda2SGreg Roach } elseif ($parts[0] === 'FAMS') { 741dfb2cda2SGreg Roach // e.g. searches for occupation, religion, note, etc. 742dfb2cda2SGreg Roach // Initial matching only. Need PHP to apply filter. 7433c480c6bSGreg Roach $query->where('spouse_families.f_gedcom', 'LIKE', "%\n1 " . $parts[1] . ' %' . $field_value . '%'); 744dfb2cda2SGreg Roach } elseif ($parts[1] === 'TYPE') { 745dfb2cda2SGreg Roach // e.g. FACT:TYPE or EVEN:TYPE 746dfb2cda2SGreg Roach // Initial matching only. Need PHP to apply filter. 747dfb2cda2SGreg Roach $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%\n2 TYPE %' . $field_value . '%'); 748dfb2cda2SGreg Roach } else { 749dfb2cda2SGreg Roach // e.g. searches for occupation, religion, note, etc. 750dfb2cda2SGreg Roach // Initial matching only. Need PHP to apply filter. 751c5457947SGreg Roach $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%' . $parts[1] . '%' . $field_value . '%'); 752dfb2cda2SGreg Roach } 753dfb2cda2SGreg Roach } 754dfb2cda2SGreg Roach return $query 755dfb2cda2SGreg Roach ->get() 75652a8ef61SGreg Roach ->each($this->rowLimiter()) 757d5ad3db0SGreg Roach ->map($this->individualRowMapper()) 758dfb2cda2SGreg Roach ->filter(GedcomRecord::accessFilter()) 7590b5fd0a6SGreg Roach ->filter(static function (Individual $individual) use ($fields): bool { 760c5457947SGreg Roach // Check for searches which were only partially matched by SQL 761dfb2cda2SGreg Roach foreach ($fields as $field_name => $field_value) { 762a41e65f0SGreg Roach $regex = '/' . preg_quote($field_value, '/') . '/i'; 763dfb2cda2SGreg Roach 764c5457947SGreg Roach $parts = explode(':', $field_name . '::::'); 765dfb2cda2SGreg Roach 766dfb2cda2SGreg Roach // *:PLAC 767c5457947SGreg Roach if ($parts[1] === 'PLAC') { 768a41e65f0SGreg Roach foreach ($individual->facts([$parts[0]]) as $fact) { 769392561bbSGreg Roach if (preg_match($regex, $fact->place()->gedcomName())) { 770c5457947SGreg Roach continue 2; 771dfb2cda2SGreg Roach } 772a41e65f0SGreg Roach } 773c5457947SGreg Roach return false; 774c5457947SGreg Roach } 775c5457947SGreg Roach 776dfb2cda2SGreg Roach // FAMS:*:PLAC 777c5457947SGreg Roach if ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') { 77839ca88baSGreg Roach foreach ($individual->spouseFamilies() as $family) { 779a41e65f0SGreg Roach foreach ($family->facts([$parts[1]]) as $fact) { 780392561bbSGreg Roach if (preg_match($regex, $fact->place()->gedcomName())) { 781c5457947SGreg Roach continue 2; 782a41e65f0SGreg Roach } 783dfb2cda2SGreg Roach } 784dfb2cda2SGreg Roach } 785c5457947SGreg Roach return false; 786c5457947SGreg Roach } 787c5457947SGreg Roach 788dfb2cda2SGreg Roach // e.g. searches for occupation, religion, note, etc. 789c5457947SGreg Roach if ($parts[0] === 'FAMS') { 79039ca88baSGreg Roach foreach ($individual->spouseFamilies() as $family) { 791a41e65f0SGreg Roach foreach ($family->facts([$parts[1]]) as $fact) { 792a41e65f0SGreg Roach if (preg_match($regex, $fact->value())) { 793c5457947SGreg Roach continue 3; 794a41e65f0SGreg Roach } 795dfb2cda2SGreg Roach } 796dfb2cda2SGreg Roach } 797c5457947SGreg Roach return false; 798c5457947SGreg Roach } 799c5457947SGreg Roach 800dfb2cda2SGreg Roach // e.g. FACT:TYPE or EVEN:TYPE 801c5457947SGreg Roach if ($parts[1] === 'TYPE' || $parts[1] === '_WT_USER') { 802a41e65f0SGreg Roach foreach ($individual->facts([$parts[0]]) as $fact) { 803c5457947SGreg Roach if (preg_match($regex, $fact->attribute($parts[1]))) { 804c5457947SGreg Roach continue 2; 805dfb2cda2SGreg Roach } 806dfb2cda2SGreg Roach } 807dfb2cda2SGreg Roach 808dfb2cda2SGreg Roach return false; 809dfb2cda2SGreg Roach } 810c5457947SGreg Roach } 811dfb2cda2SGreg Roach 812dfb2cda2SGreg Roach return true; 813dfb2cda2SGreg Roach }); 814dfb2cda2SGreg Roach } 815dfb2cda2SGreg Roach 816dfb2cda2SGreg Roach /** 817dfb2cda2SGreg Roach * @param string $soundex 818dfb2cda2SGreg Roach * @param string $lastname 819dfb2cda2SGreg Roach * @param string $firstname 820dfb2cda2SGreg Roach * @param string $place 821dfb2cda2SGreg Roach * @param Tree[] $search_trees 822dfb2cda2SGreg Roach * 823b5c8fd7eSGreg Roach * @return Collection<Individual> 824dfb2cda2SGreg Roach */ 825dfb2cda2SGreg Roach public function searchIndividualsPhonetic(string $soundex, string $lastname, string $firstname, string $place, array $search_trees): Collection 826dfb2cda2SGreg Roach { 827dfb2cda2SGreg Roach switch ($soundex) { 828dfb2cda2SGreg Roach default: 829dfb2cda2SGreg Roach case 'Russell': 830dfb2cda2SGreg Roach $givn_sdx = Soundex::russell($firstname); 831dfb2cda2SGreg Roach $surn_sdx = Soundex::russell($lastname); 832dfb2cda2SGreg Roach $plac_sdx = Soundex::russell($place); 833dfb2cda2SGreg Roach $givn_field = 'n_soundex_givn_std'; 834dfb2cda2SGreg Roach $surn_field = 'n_soundex_surn_std'; 835dfb2cda2SGreg Roach $plac_field = 'p_std_soundex'; 836dfb2cda2SGreg Roach break; 837dfb2cda2SGreg Roach case 'DaitchM': 838dfb2cda2SGreg Roach $givn_sdx = Soundex::daitchMokotoff($firstname); 839dfb2cda2SGreg Roach $surn_sdx = Soundex::daitchMokotoff($lastname); 840dfb2cda2SGreg Roach $plac_sdx = Soundex::daitchMokotoff($place); 841dfb2cda2SGreg Roach $givn_field = 'n_soundex_givn_dm'; 842dfb2cda2SGreg Roach $surn_field = 'n_soundex_surn_dm'; 843dfb2cda2SGreg Roach $plac_field = 'p_dm_soundex'; 844dfb2cda2SGreg Roach break; 845dfb2cda2SGreg Roach } 846dfb2cda2SGreg Roach 847dfb2cda2SGreg Roach // Nothing to search for? Return nothing. 848dfb2cda2SGreg Roach if ($givn_sdx === '' && $surn_sdx === '' && $plac_sdx === '') { 8493fda39a7SGreg Roach return new Collection(); 850dfb2cda2SGreg Roach } 851dfb2cda2SGreg Roach 852dfb2cda2SGreg Roach $query = DB::table('individuals') 853dfb2cda2SGreg Roach ->select(['individuals.*']) 854dfb2cda2SGreg Roach ->distinct(); 855dfb2cda2SGreg Roach 856dfb2cda2SGreg Roach $this->whereTrees($query, 'i_file', $search_trees); 857dfb2cda2SGreg Roach 858dfb2cda2SGreg Roach if ($plac_sdx !== '') { 8590b5fd0a6SGreg Roach $query->join('placelinks', static function (JoinClause $join): void { 860dfb2cda2SGreg Roach $join 861dfb2cda2SGreg Roach ->on('placelinks.pl_file', '=', 'individuals.i_file') 862dfb2cda2SGreg Roach ->on('placelinks.pl_gid', '=', 'individuals.i_id'); 863dfb2cda2SGreg Roach }); 8640b5fd0a6SGreg Roach $query->join('places', static function (JoinClause $join): void { 865dfb2cda2SGreg Roach $join 866dfb2cda2SGreg Roach ->on('places.p_file', '=', 'placelinks.pl_file') 867dfb2cda2SGreg Roach ->on('places.p_id', '=', 'placelinks.pl_p_id'); 868dfb2cda2SGreg Roach }); 869dfb2cda2SGreg Roach 870dfb2cda2SGreg Roach $this->wherePhonetic($query, $plac_field, $plac_sdx); 871dfb2cda2SGreg Roach } 872dfb2cda2SGreg Roach 873dfb2cda2SGreg Roach if ($givn_sdx !== '' || $surn_sdx !== '') { 8740b5fd0a6SGreg Roach $query->join('name', static function (JoinClause $join): void { 875dfb2cda2SGreg Roach $join 876dfb2cda2SGreg Roach ->on('name.n_file', '=', 'individuals.i_file') 877dfb2cda2SGreg Roach ->on('name.n_id', '=', 'individuals.i_id'); 878dfb2cda2SGreg Roach }); 879dfb2cda2SGreg Roach 880dfb2cda2SGreg Roach $this->wherePhonetic($query, $givn_field, $givn_sdx); 881dfb2cda2SGreg Roach $this->wherePhonetic($query, $surn_field, $surn_sdx); 882dfb2cda2SGreg Roach } 883dfb2cda2SGreg Roach 884dfb2cda2SGreg Roach return $query 885dfb2cda2SGreg Roach ->get() 88652a8ef61SGreg Roach ->each($this->rowLimiter()) 887d5ad3db0SGreg Roach ->map($this->individualRowMapper()) 888dfb2cda2SGreg Roach ->filter(GedcomRecord::accessFilter()); 889dfb2cda2SGreg Roach } 890dfb2cda2SGreg Roach 891dfb2cda2SGreg Roach /** 89232cd2800SGreg Roach * Paginate a search query. 89332cd2800SGreg Roach * 89432cd2800SGreg Roach * @param Builder $query Searches the database for the desired records. 89532cd2800SGreg Roach * @param Closure $row_mapper Converts a row from the query into a record. 896a7a24840SGreg Roach * @param Closure $row_filter 89732cd2800SGreg Roach * @param int $offset Skip this many rows. 89832cd2800SGreg Roach * @param int $limit Take this many rows. 89932cd2800SGreg Roach * 900b5c8fd7eSGreg Roach * @return Collection<mixed> 90132cd2800SGreg Roach */ 902a7a24840SGreg Roach private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection 90332cd2800SGreg Roach { 90432cd2800SGreg Roach $collection = new Collection(); 90532cd2800SGreg Roach 90632cd2800SGreg Roach foreach ($query->cursor() as $row) { 90732cd2800SGreg Roach $record = $row_mapper($row); 908b68caec6SGreg Roach // If the object has a method "canShow()", then use it to filter for privacy. 909a7a24840SGreg Roach if ($row_filter($record)) { 91032cd2800SGreg Roach if ($offset > 0) { 91132cd2800SGreg Roach $offset--; 91232cd2800SGreg Roach } else { 91332cd2800SGreg Roach if ($limit > 0) { 91432cd2800SGreg Roach $collection->push($record); 91532cd2800SGreg Roach } 91632cd2800SGreg Roach 91732cd2800SGreg Roach $limit--; 91832cd2800SGreg Roach 91932cd2800SGreg Roach if ($limit === 0) { 92032cd2800SGreg Roach break; 92132cd2800SGreg Roach } 92232cd2800SGreg Roach } 92332cd2800SGreg Roach } 92432cd2800SGreg Roach } 92532cd2800SGreg Roach 926e0458bdcSGreg Roach 92732cd2800SGreg Roach return $collection; 92832cd2800SGreg Roach } 929a7a24840SGreg Roach 930a7a24840SGreg Roach /** 931a7a24840SGreg Roach * Apply search filters to a SQL query column. Apply collation rules to MySQL. 932a7a24840SGreg Roach * 933a7a24840SGreg Roach * @param Builder $query 934a7a24840SGreg Roach * @param Expression|string $field 935a7a24840SGreg Roach * @param string[] $search_terms 936a7a24840SGreg Roach */ 937a7a24840SGreg Roach private function whereSearch(Builder $query, $field, array $search_terms): void 938a7a24840SGreg Roach { 939a7a24840SGreg Roach if ($field instanceof Expression) { 940a7a24840SGreg Roach $field = $field->getValue(); 941a7a24840SGreg Roach } 942a7a24840SGreg Roach 943a7a24840SGreg Roach foreach ($search_terms as $search_term) { 944a69f5655SGreg Roach $query->whereContains(new Expression($field), $search_term); 945a7a24840SGreg Roach } 946a7a24840SGreg Roach } 947a7a24840SGreg Roach 948a7a24840SGreg Roach /** 9492d686e68SGreg Roach * Apply soundex search filters to a SQL query column. 9502d686e68SGreg Roach * 9512d686e68SGreg Roach * @param Builder $query 9522d686e68SGreg Roach * @param Expression|string $field 9532d686e68SGreg Roach * @param string $soundex 9542d686e68SGreg Roach */ 9552d686e68SGreg Roach private function wherePhonetic(Builder $query, $field, string $soundex): void 9562d686e68SGreg Roach { 9572d686e68SGreg Roach if ($soundex !== '') { 9580b5fd0a6SGreg Roach $query->where(static function (Builder $query) use ($soundex, $field): void { 9592d686e68SGreg Roach foreach (explode(':', $soundex) as $sdx) { 9602d686e68SGreg Roach $query->orWhere($field, 'LIKE', '%' . $sdx . '%'); 9612d686e68SGreg Roach } 9622d686e68SGreg Roach }); 9632d686e68SGreg Roach } 9642d686e68SGreg Roach } 9652d686e68SGreg Roach 9662d686e68SGreg Roach /** 967a7a24840SGreg Roach * @param Builder $query 968a7a24840SGreg Roach * @param string $tree_id_field 969a7a24840SGreg Roach * @param Tree[] $trees 970a7a24840SGreg Roach */ 971a7a24840SGreg Roach private function whereTrees(Builder $query, string $tree_id_field, array $trees): void 972a7a24840SGreg Roach { 9730b5fd0a6SGreg Roach $tree_ids = array_map(static function (Tree $tree): int { 974a7a24840SGreg Roach return $tree->id(); 975a7a24840SGreg Roach }, $trees); 976a7a24840SGreg Roach 977a7a24840SGreg Roach $query->whereIn($tree_id_field, $tree_ids); 978a7a24840SGreg Roach } 979a7a24840SGreg Roach 980d5ad3db0SGreg Roach /** 981d5ad3db0SGreg Roach * Find the media object that uses a particular media file. 982d5ad3db0SGreg Roach * 983d5ad3db0SGreg Roach * @param string $file 984d5ad3db0SGreg Roach * 985d5ad3db0SGreg Roach * @return Media[] 986d5ad3db0SGreg Roach */ 987d5ad3db0SGreg Roach public function findMediaObjectsForMediaFile(string $file): array 988d5ad3db0SGreg Roach { 989d5ad3db0SGreg Roach return DB::table('media') 990d5ad3db0SGreg Roach ->join('media_file', static function (JoinClause $join): void { 991d5ad3db0SGreg Roach $join 992d5ad3db0SGreg Roach ->on('media_file.m_file', '=', 'media.m_file') 993d5ad3db0SGreg Roach ->on('media_file.m_id', '=', 'media.m_id'); 994d5ad3db0SGreg Roach }) 995d5ad3db0SGreg Roach ->join('gedcom_setting', 'media.m_file', '=', 'gedcom_setting.gedcom_id') 996d5ad3db0SGreg Roach ->where(new Expression('setting_value || multimedia_file_refn'), '=', $file) 997d5ad3db0SGreg Roach ->select(['media.*']) 998d5ad3db0SGreg Roach ->distinct() 999d5ad3db0SGreg Roach ->get() 1000d5ad3db0SGreg Roach ->map($this->mediaRowMapper()) 1001d5ad3db0SGreg Roach ->all(); 1002d5ad3db0SGreg Roach } 1003d5ad3db0SGreg Roach 1004a7a24840SGreg Roach /** 1005a7a24840SGreg Roach * A closure to filter records by privacy-filtered GEDCOM data. 1006a7a24840SGreg Roach * 1007a7a24840SGreg Roach * @param array $search_terms 1008a7a24840SGreg Roach * 1009a7a24840SGreg Roach * @return Closure 1010a7a24840SGreg Roach */ 1011a7a24840SGreg Roach private function rawGedcomFilter(array $search_terms): Closure 1012a7a24840SGreg Roach { 10136c2179e2SGreg Roach return static function (GedcomRecord $record) use ($search_terms): bool { 1014a7a24840SGreg Roach // Ignore non-genealogy fields 1015faa5e163SGreg Roach $gedcom = preg_replace('/\n\d (?:_UID|_WT_USER) .*/', '', $record->gedcom()); 1016a7a24840SGreg Roach 1017a7a24840SGreg Roach // Ignore matches in links 1018a7a24840SGreg Roach $gedcom = preg_replace('/\n\d ' . Gedcom::REGEX_TAG . '( @' . Gedcom::REGEX_XREF . '@)?/', '', $gedcom); 1019a7a24840SGreg Roach 1020a7a24840SGreg Roach // Re-apply the filtering 1021a7a24840SGreg Roach foreach ($search_terms as $search_term) { 1022a7a24840SGreg Roach if (mb_stripos($gedcom, $search_term) === false) { 1023a7a24840SGreg Roach return false; 1024a7a24840SGreg Roach } 1025a7a24840SGreg Roach } 1026a7a24840SGreg Roach 1027a7a24840SGreg Roach return true; 1028a7a24840SGreg Roach }; 1029a7a24840SGreg Roach } 103052a8ef61SGreg Roach 103152a8ef61SGreg Roach /** 103252a8ef61SGreg Roach * Searching for short or common text can give more results than the system can process. 103352a8ef61SGreg Roach * 103452a8ef61SGreg Roach * @param int $limit 103552a8ef61SGreg Roach * 103652a8ef61SGreg Roach * @return Closure 103752a8ef61SGreg Roach */ 103852a8ef61SGreg Roach private function rowLimiter(int $limit = 1000): Closure 103952a8ef61SGreg Roach { 10406c2179e2SGreg Roach return static function () use ($limit): void { 104152a8ef61SGreg Roach static $n = 0; 104252a8ef61SGreg Roach 104352a8ef61SGreg Roach if (++$n > $limit) { 104452a8ef61SGreg Roach $message = I18N::translate('The search returned too many results.'); 104552a8ef61SGreg Roach 1046d501c45dSGreg Roach throw new HttpServiceUnavailableException($message); 104752a8ef61SGreg Roach } 104852a8ef61SGreg Roach }; 104952a8ef61SGreg Roach } 1050d5ad3db0SGreg Roach 1051d5ad3db0SGreg Roach /** 1052d5ad3db0SGreg Roach * Convert a row from any tree in the families table into a family object. 1053d5ad3db0SGreg Roach * 1054d5ad3db0SGreg Roach * @return Closure 1055d5ad3db0SGreg Roach */ 1056d5ad3db0SGreg Roach private function familyRowMapper(): Closure 1057d5ad3db0SGreg Roach { 1058d5ad3db0SGreg Roach return function (stdClass $row): Family { 1059d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->f_file); 1060d5ad3db0SGreg Roach 1061d5ad3db0SGreg Roach return Family::getInstance($row->f_id, $tree, $row->f_gedcom); 1062d5ad3db0SGreg Roach }; 1063d5ad3db0SGreg Roach } 1064d5ad3db0SGreg Roach 1065d5ad3db0SGreg Roach /** 1066d5ad3db0SGreg Roach * Convert a row from any tree in the individuals table into an individual object. 1067d5ad3db0SGreg Roach * 1068d5ad3db0SGreg Roach * @return Closure 1069d5ad3db0SGreg Roach */ 1070d5ad3db0SGreg Roach private function individualRowMapper(): Closure 1071d5ad3db0SGreg Roach { 1072d5ad3db0SGreg Roach return function (stdClass $row): Individual { 1073d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->i_file); 1074d5ad3db0SGreg Roach 1075d5ad3db0SGreg Roach return Individual::getInstance($row->i_id, $tree, $row->i_gedcom); 1076d5ad3db0SGreg Roach }; 1077d5ad3db0SGreg Roach } 1078d5ad3db0SGreg Roach 1079d5ad3db0SGreg Roach /** 1080d5ad3db0SGreg Roach * Convert a row from any tree in the media table into an media object. 1081d5ad3db0SGreg Roach * 1082d5ad3db0SGreg Roach * @return Closure 1083d5ad3db0SGreg Roach */ 1084d5ad3db0SGreg Roach private function mediaRowMapper(): Closure 1085d5ad3db0SGreg Roach { 1086d5ad3db0SGreg Roach return function (stdClass $row): Media { 1087d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->m_file); 1088d5ad3db0SGreg Roach 1089d5ad3db0SGreg Roach return Media::getInstance($row->m_id, $tree, $row->m_gedcom); 1090d5ad3db0SGreg Roach }; 1091d5ad3db0SGreg Roach } 1092d5ad3db0SGreg Roach 1093d5ad3db0SGreg Roach /** 1094d5ad3db0SGreg Roach * Convert a row from any tree in the other table into a note object. 1095d5ad3db0SGreg Roach * 1096d5ad3db0SGreg Roach * @return Closure 1097d5ad3db0SGreg Roach */ 1098d5ad3db0SGreg Roach private function noteRowMapper(): Closure 1099d5ad3db0SGreg Roach { 1100d5ad3db0SGreg Roach return function (stdClass $row): Note { 1101d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->o_file); 1102d5ad3db0SGreg Roach 1103d5ad3db0SGreg Roach return Note::getInstance($row->o_id, $tree, $row->o_gedcom); 1104d5ad3db0SGreg Roach }; 1105d5ad3db0SGreg Roach } 1106d5ad3db0SGreg Roach 1107d5ad3db0SGreg Roach /** 1108d5ad3db0SGreg Roach * Convert a row from any tree in the other table into a repository object. 1109d5ad3db0SGreg Roach * 1110d5ad3db0SGreg Roach * @return Closure 1111d5ad3db0SGreg Roach */ 1112d5ad3db0SGreg Roach private function repositoryRowMapper(): Closure 1113d5ad3db0SGreg Roach { 1114d5ad3db0SGreg Roach return function (stdClass $row): Repository { 1115d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->o_file); 1116d5ad3db0SGreg Roach 1117d5ad3db0SGreg Roach return Repository::getInstance($row->o_id, $tree, $row->o_gedcom); 1118d5ad3db0SGreg Roach }; 1119d5ad3db0SGreg Roach } 1120d5ad3db0SGreg Roach 1121d5ad3db0SGreg Roach /** 1122d5ad3db0SGreg Roach * Convert a row from any tree in the sources table into a source object. 1123d5ad3db0SGreg Roach * 1124d5ad3db0SGreg Roach * @return Closure 1125d5ad3db0SGreg Roach */ 1126d5ad3db0SGreg Roach private function sourceRowMapper(): Closure 1127d5ad3db0SGreg Roach { 1128d5ad3db0SGreg Roach return function (stdClass $row): Source { 1129d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->s_file); 1130d5ad3db0SGreg Roach 1131d5ad3db0SGreg Roach return Source::getInstance($row->s_id, $tree, $row->s_gedcom); 1132d5ad3db0SGreg Roach }; 1133d5ad3db0SGreg Roach } 1134d5ad3db0SGreg Roach 1135d5ad3db0SGreg Roach /** 1136d5ad3db0SGreg Roach * Convert a row from any tree in the other table into a submitter object. 1137d5ad3db0SGreg Roach * 1138d5ad3db0SGreg Roach * @return Closure 1139d5ad3db0SGreg Roach */ 1140d5ad3db0SGreg Roach private function submitterRowMapper(): Closure 1141d5ad3db0SGreg Roach { 1142d5ad3db0SGreg Roach return function (stdClass $row): GedcomRecord { 1143d5ad3db0SGreg Roach $tree = $this->tree_service->find((int) $row->o_file); 1144d5ad3db0SGreg Roach 1145d5ad3db0SGreg Roach return GedcomRecord::getInstance($row->o_id, $tree, $row->o_gedcom); 1146d5ad3db0SGreg Roach }; 1147d5ad3db0SGreg Roach } 114832cd2800SGreg Roach} 1149