xref: /webtrees/app/Services/SearchService.php (revision dfb2cda21fe79e99de32647671ecf897ee37a81e)
132cd2800SGreg Roach<?php
232cd2800SGreg Roach/**
332cd2800SGreg Roach * webtrees: online genealogy
432cd2800SGreg Roach * Copyright (C) 2019 webtrees development team
532cd2800SGreg Roach * This program is free software: you can redistribute it and/or modify
632cd2800SGreg Roach * it under the terms of the GNU General Public License as published by
732cd2800SGreg Roach * the Free Software Foundation, either version 3 of the License, or
832cd2800SGreg Roach * (at your option) any later version.
932cd2800SGreg Roach * This program is distributed in the hope that it will be useful,
1032cd2800SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1132cd2800SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1232cd2800SGreg Roach * GNU General Public License for more details.
1332cd2800SGreg Roach * You should have received a copy of the GNU General Public License
1432cd2800SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
1532cd2800SGreg Roach */
1632cd2800SGreg Roachdeclare(strict_types=1);
1732cd2800SGreg Roach
1832cd2800SGreg Roachnamespace Fisharebest\Webtrees\Services;
1932cd2800SGreg Roach
2032cd2800SGreg Roachuse Closure;
21a7a24840SGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface;
22*dfb2cda2SGreg Roachuse Fisharebest\Webtrees\Date;
2332cd2800SGreg Roachuse Fisharebest\Webtrees\Family;
24a7a24840SGreg Roachuse Fisharebest\Webtrees\Gedcom;
2532cd2800SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2632cd2800SGreg Roachuse Fisharebest\Webtrees\Individual;
2732cd2800SGreg Roachuse Fisharebest\Webtrees\Media;
2832cd2800SGreg Roachuse Fisharebest\Webtrees\Note;
29b68caec6SGreg Roachuse Fisharebest\Webtrees\Place;
3032cd2800SGreg Roachuse Fisharebest\Webtrees\Repository;
312d686e68SGreg Roachuse Fisharebest\Webtrees\Soundex;
3232cd2800SGreg Roachuse Fisharebest\Webtrees\Source;
3332cd2800SGreg Roachuse Fisharebest\Webtrees\Tree;
3432cd2800SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
3532cd2800SGreg Roachuse Illuminate\Database\Query\Builder;
36a7a24840SGreg Roachuse Illuminate\Database\Query\Expression;
3732cd2800SGreg Roachuse Illuminate\Database\Query\JoinClause;
3832cd2800SGreg Roachuse Illuminate\Support\Collection;
3932cd2800SGreg Roachuse stdClass;
40a7a24840SGreg Roachuse function mb_stripos;
4132cd2800SGreg Roach
4232cd2800SGreg Roach/**
4332cd2800SGreg Roach * Search trees for genealogy records.
4432cd2800SGreg Roach */
4532cd2800SGreg Roachclass SearchService
4632cd2800SGreg Roach{
47a7a24840SGreg Roach    /** @var LocaleInterface */
48a7a24840SGreg Roach    private $locale;
49a7a24840SGreg Roach
50a7a24840SGreg Roach    /**
51a7a24840SGreg Roach     * SearchService constructor.
52a7a24840SGreg Roach     *
53a7a24840SGreg Roach     * @param LocaleInterface $locale
54a7a24840SGreg Roach     */
55a7a24840SGreg Roach    public function __construct(LocaleInterface $locale)
56a7a24840SGreg Roach    {
57a7a24840SGreg Roach        $this->locale = $locale;
58a7a24840SGreg Roach    }
59a7a24840SGreg Roach
60a7a24840SGreg Roach    /**
61a7a24840SGreg Roach     * @param Tree[]   $trees
62a7a24840SGreg Roach     * @param string[] $search
63a7a24840SGreg Roach     *
64a7a24840SGreg Roach     * @return Collection|Family[]
65a7a24840SGreg Roach     */
66a7a24840SGreg Roach    public function searchFamilies(array $trees, array $search): Collection
67a7a24840SGreg Roach    {
68a7a24840SGreg Roach        $query = DB::table('families');
69a7a24840SGreg Roach
70a7a24840SGreg Roach        $this->whereTrees($query, 'f_file', $trees);
71a7a24840SGreg Roach        $this->whereSearch($query, 'f_gedcom', $search);
72a7a24840SGreg Roach
73a7a24840SGreg Roach        return $query
74a7a24840SGreg Roach            ->get()
75a7a24840SGreg Roach            ->map(Family::rowMapper())
76a7a24840SGreg Roach            ->filter(GedcomRecord::accessFilter())
777f5fa3c2SGreg Roach            ->filter($this->rawGedcomFilter($search));
78a7a24840SGreg Roach    }
79a7a24840SGreg Roach
8032cd2800SGreg Roach    /**
8132cd2800SGreg Roach     * Search for families by name.
8232cd2800SGreg Roach     *
83a7a24840SGreg Roach     * @param Tree[]   $trees
84a7a24840SGreg Roach     * @param string[] $search
8532cd2800SGreg Roach     * @param int      $offset
8632cd2800SGreg Roach     * @param int      $limit
8732cd2800SGreg Roach     *
8832cd2800SGreg Roach     * @return Collection|Family[]
8932cd2800SGreg Roach     */
90a7a24840SGreg Roach    public function searchFamilyNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
9132cd2800SGreg Roach    {
9232cd2800SGreg Roach        $query = DB::table('families')
93a7a24840SGreg Roach            ->join('name AS husb_name', function (JoinClause $join): void {
9432cd2800SGreg Roach                $join
9532cd2800SGreg Roach                    ->on('husb_name.n_file', '=', 'families.f_file')
96a7a24840SGreg Roach                    ->on('husb_name.n_id', '=', 'families.f_husb');
9732cd2800SGreg Roach            })
98a7a24840SGreg Roach            ->join('name AS wife_name', function (JoinClause $join): void {
9932cd2800SGreg Roach                $join
10032cd2800SGreg Roach                    ->on('wife_name.n_file', '=', 'families.f_file')
101a7a24840SGreg Roach                    ->on('wife_name.n_id', '=', 'families.f_wife');
10232cd2800SGreg Roach            })
103a7a24840SGreg Roach            ->where('wife_name.n_type', '<>', '_MARNM')
104a7a24840SGreg Roach            ->where('husb_name.n_type', '<>', '_MARNM');
105a7a24840SGreg Roach
106a7a24840SGreg Roach        $prefix = DB::connection()->getTablePrefix();
107e3bddf11SGreg Roach        $field  = DB::raw($prefix . '(husb_name.n_full || ' . $prefix . 'wife_name.n_full)');
108a7a24840SGreg Roach
109a7a24840SGreg Roach        $this->whereTrees($query, 'f_file', $trees);
110a7a24840SGreg Roach        $this->whereSearch($query, $field, $search);
111a7a24840SGreg Roach
112a7a24840SGreg Roach        $query
11332cd2800SGreg Roach            ->orderBy('husb_name.n_sort')
11432cd2800SGreg Roach            ->orderBy('wife_name.n_sort')
115c0804649SGreg Roach            ->select(['families.*', 'husb_name.n_sort', 'wife_name.n_sort'])
11632cd2800SGreg Roach            ->distinct();
11732cd2800SGreg Roach
118a7a24840SGreg Roach        return $this->paginateQuery($query, Family::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
119a7a24840SGreg Roach    }
120a7a24840SGreg Roach
121a7a24840SGreg Roach    /**
122a7a24840SGreg Roach     * @param Tree[]   $trees
123a7a24840SGreg Roach     * @param string[] $search
124a7a24840SGreg Roach     *
125a7a24840SGreg Roach     * @return Collection|Individual[]
126a7a24840SGreg Roach     */
127a7a24840SGreg Roach    public function searchIndividuals(array $trees, array $search): Collection
128a7a24840SGreg Roach    {
129a7a24840SGreg Roach        $query = DB::table('individuals');
130a7a24840SGreg Roach
131a7a24840SGreg Roach        $this->whereTrees($query, 'i_file', $trees);
132a7a24840SGreg Roach        $this->whereSearch($query, 'i_gedcom', $search);
133a7a24840SGreg Roach
134a7a24840SGreg Roach        return $query
135a7a24840SGreg Roach            ->get()
136a7a24840SGreg Roach            ->map(Individual::rowMapper())
137a7a24840SGreg Roach            ->filter(GedcomRecord::accessFilter())
1387f5fa3c2SGreg Roach            ->filter($this->rawGedcomFilter($search));
13932cd2800SGreg Roach    }
14032cd2800SGreg Roach
14132cd2800SGreg Roach    /**
14232cd2800SGreg Roach     * Search for individuals by name.
14332cd2800SGreg Roach     *
144a7a24840SGreg Roach     * @param Tree[]   $trees
145a7a24840SGreg Roach     * @param string[] $search
14632cd2800SGreg Roach     * @param int      $offset
14732cd2800SGreg Roach     * @param int      $limit
14832cd2800SGreg Roach     *
14932cd2800SGreg Roach     * @return Collection|Individual[]
15032cd2800SGreg Roach     */
151a7a24840SGreg Roach    public function searchIndividualNames(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
15232cd2800SGreg Roach    {
15332cd2800SGreg Roach        $query = DB::table('individuals')
154a7a24840SGreg Roach            ->join('name', function (JoinClause $join): void {
15532cd2800SGreg Roach                $join
15632cd2800SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
157a7a24840SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id');
15832cd2800SGreg Roach            })
159e84cf2deSGreg Roach            ->orderBy('n_sort')
160c0804649SGreg Roach            ->select(['individuals.*', 'n_sort', 'n_num'])
16132cd2800SGreg Roach            ->distinct();
16232cd2800SGreg Roach
163a7a24840SGreg Roach        $this->whereTrees($query, 'i_file', $trees);
164a7a24840SGreg Roach        $this->whereSearch($query, 'n_full', $search);
165a7a24840SGreg Roach
166a7a24840SGreg Roach        return $this->paginateQuery($query, Individual::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
16732cd2800SGreg Roach    }
16832cd2800SGreg Roach
16932cd2800SGreg Roach    /**
17032cd2800SGreg Roach     * Search for media objects.
17132cd2800SGreg Roach     *
172a7a24840SGreg Roach     * @param Tree[]   $trees
173a7a24840SGreg Roach     * @param string[] $search
17432cd2800SGreg Roach     * @param int      $offset
17532cd2800SGreg Roach     * @param int      $limit
17632cd2800SGreg Roach     *
17732cd2800SGreg Roach     * @return Collection|Media[]
17832cd2800SGreg Roach     */
179a7a24840SGreg Roach    public function searchMedia(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
18032cd2800SGreg Roach    {
181a7a24840SGreg Roach        $query = DB::table('media');
18232cd2800SGreg Roach
183a7a24840SGreg Roach        $this->whereTrees($query, 'media.m_file', $trees);
184a7a24840SGreg Roach        $this->whereSearch($query, 'm_gedcom', $search);
185a7a24840SGreg Roach
186a7a24840SGreg Roach        return $this->paginateQuery($query, Media::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
18732cd2800SGreg Roach    }
18832cd2800SGreg Roach
18932cd2800SGreg Roach    /**
19032cd2800SGreg Roach     * Search for notes.
19132cd2800SGreg Roach     *
192a7a24840SGreg Roach     * @param Tree[]   $trees
193a7a24840SGreg Roach     * @param string[] $search
19432cd2800SGreg Roach     * @param int      $offset
19532cd2800SGreg Roach     * @param int      $limit
19632cd2800SGreg Roach     *
19732cd2800SGreg Roach     * @return Collection|Note[]
19832cd2800SGreg Roach     */
199a7a24840SGreg Roach    public function searchNotes(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
20032cd2800SGreg Roach    {
20132cd2800SGreg Roach        $query = DB::table('other')
202a7a24840SGreg Roach            ->where('o_type', '=', 'NOTE');
20332cd2800SGreg Roach
204a7a24840SGreg Roach        $this->whereTrees($query, 'o_file', $trees);
205a7a24840SGreg Roach        $this->whereSearch($query, 'o_gedcom', $search);
206a7a24840SGreg Roach
207a7a24840SGreg Roach        return $this->paginateQuery($query, Note::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
20832cd2800SGreg Roach    }
20932cd2800SGreg Roach
21032cd2800SGreg Roach    /**
21132cd2800SGreg Roach     * Search for repositories.
21232cd2800SGreg Roach     *
213a7a24840SGreg Roach     * @param Tree[]   $trees
214a7a24840SGreg Roach     * @param string[] $search
21532cd2800SGreg Roach     * @param int      $offset
21632cd2800SGreg Roach     * @param int      $limit
21732cd2800SGreg Roach     *
21832cd2800SGreg Roach     * @return Collection|Repository[]
21932cd2800SGreg Roach     */
220a7a24840SGreg Roach    public function searchRepositories(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
22132cd2800SGreg Roach    {
22232cd2800SGreg Roach        $query = DB::table('other')
223a7a24840SGreg Roach            ->where('o_type', '=', 'REPO');
22432cd2800SGreg Roach
225a7a24840SGreg Roach        $this->whereTrees($query, 'o_file', $trees);
226a7a24840SGreg Roach        $this->whereSearch($query, 'o_gedcom', $search);
227a7a24840SGreg Roach
228a7a24840SGreg Roach        return $this->paginateQuery($query, Repository::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
22932cd2800SGreg Roach    }
23032cd2800SGreg Roach
23132cd2800SGreg Roach    /**
232a7a24840SGreg Roach     * Search for sources.
23332cd2800SGreg Roach     *
234a7a24840SGreg Roach     * @param Tree[]   $trees
235a7a24840SGreg Roach     * @param string[] $search
23632cd2800SGreg Roach     * @param int      $offset
23732cd2800SGreg Roach     * @param int      $limit
23832cd2800SGreg Roach     *
23932cd2800SGreg Roach     * @return Collection|Source[]
24032cd2800SGreg Roach     */
241a7a24840SGreg Roach    public function searchSources(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
242a7a24840SGreg Roach    {
243a7a24840SGreg Roach        $query = DB::table('sources');
244a7a24840SGreg Roach
245a7a24840SGreg Roach        $this->whereTrees($query, 's_file', $trees);
246a7a24840SGreg Roach        $this->whereSearch($query, 's_gedcom', $search);
247a7a24840SGreg Roach
248a7a24840SGreg Roach        return $this->paginateQuery($query, Source::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
249a7a24840SGreg Roach    }
250a7a24840SGreg Roach
251a7a24840SGreg Roach    /**
252a7a24840SGreg Roach     * Search for sources by name.
253a7a24840SGreg Roach     *
254a7a24840SGreg Roach     * @param Tree[]   $trees
255a7a24840SGreg Roach     * @param string[] $search
256a7a24840SGreg Roach     * @param int      $offset
257a7a24840SGreg Roach     * @param int      $limit
258a7a24840SGreg Roach     *
259a7a24840SGreg Roach     * @return Collection|Source[]
260a7a24840SGreg Roach     */
261a7a24840SGreg Roach    public function searchSourcesByName(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
26232cd2800SGreg Roach    {
26332cd2800SGreg Roach        $query = DB::table('sources')
264c0804649SGreg Roach            ->orderBy('s_name');
26532cd2800SGreg Roach
266a7a24840SGreg Roach        $this->whereTrees($query, 's_file', $trees);
267a7a24840SGreg Roach        $this->whereSearch($query, 's_name', $search);
268a7a24840SGreg Roach
269a7a24840SGreg Roach        return $this->paginateQuery($query, Source::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
27032cd2800SGreg Roach    }
27132cd2800SGreg Roach
27232cd2800SGreg Roach    /**
27332cd2800SGreg Roach     * Search for submitters.
27432cd2800SGreg Roach     *
275a7a24840SGreg Roach     * @param Tree[]   $trees
276a7a24840SGreg Roach     * @param string[] $search
27732cd2800SGreg Roach     * @param int      $offset
27832cd2800SGreg Roach     * @param int      $limit
27932cd2800SGreg Roach     *
28032cd2800SGreg Roach     * @return Collection|GedcomRecord[]
28132cd2800SGreg Roach     */
282a7a24840SGreg Roach    public function searchSubmitters(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
28332cd2800SGreg Roach    {
28432cd2800SGreg Roach        $query = DB::table('other')
285a7a24840SGreg Roach            ->where('o_type', '=', 'SUBM');
28632cd2800SGreg Roach
287a7a24840SGreg Roach        $this->whereTrees($query, 'o_file', $trees);
288a7a24840SGreg Roach        $this->whereSearch($query, 'o_gedcom', $search);
289a7a24840SGreg Roach
290a7a24840SGreg Roach        return $this->paginateQuery($query, GedcomRecord::rowMapper(), GedcomRecord::accessFilter(), $offset, $limit);
29132cd2800SGreg Roach    }
29232cd2800SGreg Roach
29332cd2800SGreg Roach    /**
294b68caec6SGreg Roach     * Search for places.
295b68caec6SGreg Roach     *
296b68caec6SGreg Roach     * @param Tree   $tree
297b68caec6SGreg Roach     * @param string $search
298b68caec6SGreg Roach     * @param int    $offset
299b68caec6SGreg Roach     * @param int    $limit
300b68caec6SGreg Roach     *
301b68caec6SGreg Roach     * @return Collection|Place[]
302b68caec6SGreg Roach     */
303b68caec6SGreg Roach    public function searchPlaces(Tree $tree, string $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection
304b68caec6SGreg Roach    {
305b68caec6SGreg Roach        $query = DB::table('places AS p0')
306b68caec6SGreg Roach            ->where('p0.p_file', '=', $tree->id())
307b68caec6SGreg Roach            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
308b68caec6SGreg Roach            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
309b68caec6SGreg Roach            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
310b68caec6SGreg Roach            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
311b68caec6SGreg Roach            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
312b68caec6SGreg Roach            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
313b68caec6SGreg Roach            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
314b68caec6SGreg Roach            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
315b68caec6SGreg Roach            ->orderBy('p0.p_place')
316b68caec6SGreg Roach            ->orderBy('p1.p_place')
317b68caec6SGreg Roach            ->orderBy('p2.p_place')
318b68caec6SGreg Roach            ->orderBy('p3.p_place')
319b68caec6SGreg Roach            ->orderBy('p4.p_place')
320b68caec6SGreg Roach            ->orderBy('p5.p_place')
321b68caec6SGreg Roach            ->orderBy('p6.p_place')
322b68caec6SGreg Roach            ->orderBy('p7.p_place')
323b68caec6SGreg Roach            ->orderBy('p8.p_place')
324b68caec6SGreg Roach            ->select([
325b68caec6SGreg Roach                'p0.p_place AS place0',
326b68caec6SGreg Roach                'p1.p_place AS place1',
327b68caec6SGreg Roach                'p2.p_place AS place2',
328b68caec6SGreg Roach                'p3.p_place AS place3',
329b68caec6SGreg Roach                'p4.p_place AS place4',
330b68caec6SGreg Roach                'p5.p_place AS place5',
331b68caec6SGreg Roach                'p6.p_place AS place6',
332b68caec6SGreg Roach                'p7.p_place AS place7',
333b68caec6SGreg Roach                'p8.p_place AS place8',
334b68caec6SGreg Roach            ]);
335b68caec6SGreg Roach
336b68caec6SGreg Roach        // Filter each level of the hierarchy.
337b68caec6SGreg Roach        foreach (explode(',', $search, 9) as $level => $string) {
338b68caec6SGreg Roach            $query->whereContains('p' . $level . '.p_place', $string);
339b68caec6SGreg Roach        }
340b68caec6SGreg Roach
341b68caec6SGreg Roach        $row_mapper = function (stdClass $row) use ($tree): Place {
342b68caec6SGreg Roach            $place = implode(', ', array_filter((array) $row));
343b68caec6SGreg Roach
344b68caec6SGreg Roach            return new Place($place, $tree);
345b68caec6SGreg Roach        };
346b68caec6SGreg Roach
347a7a24840SGreg Roach        $filter = function (): bool {
348a7a24840SGreg Roach            return true;
349a7a24840SGreg Roach        };
350b68caec6SGreg Roach
351a7a24840SGreg Roach        return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit);
352a7a24840SGreg Roach    }
353b68caec6SGreg Roach
354b68caec6SGreg Roach    /**
355*dfb2cda2SGreg Roach     * @param Tree     $tree
356*dfb2cda2SGreg Roach     * @param string[] $fields
357*dfb2cda2SGreg Roach     * @param string[] $modifiers
358*dfb2cda2SGreg Roach     *
359*dfb2cda2SGreg Roach     * @return Collection|Individual[]
360*dfb2cda2SGreg Roach     */
361*dfb2cda2SGreg Roach    public function searchIndividualsAdvanced(array $trees, array $fields, array $modifiers): Collection
362*dfb2cda2SGreg Roach    {
363*dfb2cda2SGreg Roach        $fields = array_filter($fields);
364*dfb2cda2SGreg Roach
365*dfb2cda2SGreg Roach        $query = DB::table('individuals')
366*dfb2cda2SGreg Roach            ->select(['individuals.*'])
367*dfb2cda2SGreg Roach            ->distinct();
368*dfb2cda2SGreg Roach
369*dfb2cda2SGreg Roach        $this->whereTrees($query, 'i_file', $trees);
370*dfb2cda2SGreg Roach
371*dfb2cda2SGreg Roach        // Join the following tables
372*dfb2cda2SGreg Roach        $father_name   = false;
373*dfb2cda2SGreg Roach        $mother_name   = false;
374*dfb2cda2SGreg Roach        $spouse_family = false;
375*dfb2cda2SGreg Roach        $indi_name     = false;
376*dfb2cda2SGreg Roach        $indi_date     = false;
377*dfb2cda2SGreg Roach        $fam_date      = false;
378*dfb2cda2SGreg Roach        $indi_plac     = false;
379*dfb2cda2SGreg Roach        $fam_plac      = false;
380*dfb2cda2SGreg Roach
381*dfb2cda2SGreg Roach        foreach ($fields as $field_name => $field_value) {
382*dfb2cda2SGreg Roach            if ($field_value !== '') {
383*dfb2cda2SGreg Roach                if (substr($field_name, 0, 14) === 'FAMC:HUSB:NAME') {
384*dfb2cda2SGreg Roach                    $father_name = true;
385*dfb2cda2SGreg Roach                } elseif (substr($field_name, 0, 14) === 'FAMC:WIFE:NAME') {
386*dfb2cda2SGreg Roach                    $mother_name = true;
387*dfb2cda2SGreg Roach                } elseif (substr($field_name, 0, 4) === 'NAME') {
388*dfb2cda2SGreg Roach                    $indi_name = true;
389*dfb2cda2SGreg Roach                } elseif (strpos($field_name, ':DATE') !== false) {
390*dfb2cda2SGreg Roach                    if (substr($field_name, 0, 4) === 'FAMS') {
391*dfb2cda2SGreg Roach                        $fam_date      = true;
392*dfb2cda2SGreg Roach                        $spouse_family = true;
393*dfb2cda2SGreg Roach                    } else {
394*dfb2cda2SGreg Roach                        $indi_date = true;
395*dfb2cda2SGreg Roach                    }
396*dfb2cda2SGreg Roach                } elseif (strpos($field_name, ':PLAC') !== false) {
397*dfb2cda2SGreg Roach                    if (substr($field_name, 0, 4) === 'FAMS') {
398*dfb2cda2SGreg Roach                        $fam_plac      = true;
399*dfb2cda2SGreg Roach                        $spouse_family = true;
400*dfb2cda2SGreg Roach                    } else {
401*dfb2cda2SGreg Roach                        $indi_plac = true;
402*dfb2cda2SGreg Roach                    }
403*dfb2cda2SGreg Roach                } elseif ($field_name === 'FAMS:NOTE') {
404*dfb2cda2SGreg Roach                    $spouse_family = true;
405*dfb2cda2SGreg Roach                }
406*dfb2cda2SGreg Roach            }
407*dfb2cda2SGreg Roach        }
408*dfb2cda2SGreg Roach
409*dfb2cda2SGreg Roach        if ($father_name || $mother_name) {
410*dfb2cda2SGreg Roach            $query->join('link AS l1', function (JoinClause $join): void {
411*dfb2cda2SGreg Roach                $join
412*dfb2cda2SGreg Roach                    ->on('l1.l_file', '=', 'individuals.i_file')
413*dfb2cda2SGreg Roach                    ->on('l1.l_from', '=', 'individuals.i_id')
414*dfb2cda2SGreg Roach                    ->where('l1.l_type', '=', 'FAMC');
415*dfb2cda2SGreg Roach            });
416*dfb2cda2SGreg Roach
417*dfb2cda2SGreg Roach            if ($father_name) {
418*dfb2cda2SGreg Roach                $query->join('link AS l2', function (JoinClause $join): void {
419*dfb2cda2SGreg Roach                    $join
420*dfb2cda2SGreg Roach                        ->on('l2.l_file', '=', 'l1.l_file')
421*dfb2cda2SGreg Roach                        ->on('l2.l_from', '=', 'l1.l_to')
422*dfb2cda2SGreg Roach                        ->where('l2.l_type', '=', 'HUSB');
423*dfb2cda2SGreg Roach                });
424*dfb2cda2SGreg Roach                $query->join('name AS father_name', function (JoinClause $join): void {
425*dfb2cda2SGreg Roach                    $join
426*dfb2cda2SGreg Roach                        ->on('father_name.n_file', '=', 'l2.l_file')
427*dfb2cda2SGreg Roach                        ->on('father_name.n_id', '=', 'l2.l_to');
428*dfb2cda2SGreg Roach                });
429*dfb2cda2SGreg Roach            }
430*dfb2cda2SGreg Roach
431*dfb2cda2SGreg Roach            if ($mother_name) {
432*dfb2cda2SGreg Roach                $query->join('link AS l3', function (JoinClause $join): void {
433*dfb2cda2SGreg Roach                    $join
434*dfb2cda2SGreg Roach                        ->on('l3.l_file', '=', 'l1.l_file')
435*dfb2cda2SGreg Roach                        ->on('l3.l_from', '=', 'l1.l_to')
436*dfb2cda2SGreg Roach                        ->where('l3.l_type', '=', 'WIFE');
437*dfb2cda2SGreg Roach                });
438*dfb2cda2SGreg Roach                $query->join('name AS mother_name', function (JoinClause $join): void {
439*dfb2cda2SGreg Roach                    $join
440*dfb2cda2SGreg Roach                        ->on('mother_name.n_file', '=', 'l3.l_file')
441*dfb2cda2SGreg Roach                        ->on('mother_name.n_id', '=', 'l3.l_to');
442*dfb2cda2SGreg Roach                });
443*dfb2cda2SGreg Roach            }
444*dfb2cda2SGreg Roach        }
445*dfb2cda2SGreg Roach
446*dfb2cda2SGreg Roach        if ($spouse_family) {
447*dfb2cda2SGreg Roach            $query->join('link AS l4', function (JoinClause $join): void {
448*dfb2cda2SGreg Roach                $join
449*dfb2cda2SGreg Roach                    ->on('l4.l_file', '=', 'individuals.i_file')
450*dfb2cda2SGreg Roach                    ->on('l4.l_from', '=', 'individuals.i_id')
451*dfb2cda2SGreg Roach                    ->where('l4.l_type', '=', 'FAMS');
452*dfb2cda2SGreg Roach            });
453*dfb2cda2SGreg Roach            $query->join('families AS spouse_families', function (JoinClause $join): void {
454*dfb2cda2SGreg Roach                $join
455*dfb2cda2SGreg Roach                    ->on('spouse_families.f_file', '=', 'l4.l_file')
456*dfb2cda2SGreg Roach                    ->on('spouse_families.f_id', '=', 'l4.l_to');
457*dfb2cda2SGreg Roach            });
458*dfb2cda2SGreg Roach        }
459*dfb2cda2SGreg Roach
460*dfb2cda2SGreg Roach        if ($indi_name) {
461*dfb2cda2SGreg Roach            $query->join('name AS individual_name', function (JoinClause $join): void {
462*dfb2cda2SGreg Roach                $join
463*dfb2cda2SGreg Roach                    ->on('individual_name.n_file', '=', 'individuals.i_file')
464*dfb2cda2SGreg Roach                    ->on('individual_name.n_id', '=', 'individuals.i_id');
465*dfb2cda2SGreg Roach            });
466*dfb2cda2SGreg Roach        }
467*dfb2cda2SGreg Roach
468*dfb2cda2SGreg Roach        if ($indi_date) {
469*dfb2cda2SGreg Roach            $query->join('dates AS individual_dates', function (JoinClause $join): void {
470*dfb2cda2SGreg Roach                $join
471*dfb2cda2SGreg Roach                    ->on('individual_dates.d_file', '=', 'individuals.i_file')
472*dfb2cda2SGreg Roach                    ->on('individual_dates.d_gid', '=', 'individuals.i_id');
473*dfb2cda2SGreg Roach            });
474*dfb2cda2SGreg Roach        }
475*dfb2cda2SGreg Roach
476*dfb2cda2SGreg Roach        if ($fam_date) {
477*dfb2cda2SGreg Roach            $query->join('dates AS family_dates', function (JoinClause $join): void {
478*dfb2cda2SGreg Roach                $join
479*dfb2cda2SGreg Roach                    ->on('family_dates.d_file', '=', 'spouse_families.f_file')
480*dfb2cda2SGreg Roach                    ->on('family_dates.d_gid', '=', 'spouse_families.f_id');
481*dfb2cda2SGreg Roach            });
482*dfb2cda2SGreg Roach        }
483*dfb2cda2SGreg Roach
484*dfb2cda2SGreg Roach        if ($indi_plac) {
485*dfb2cda2SGreg Roach            $query->join('placelinks AS individual_placelinks', function (JoinClause $join): void {
486*dfb2cda2SGreg Roach                $join
487*dfb2cda2SGreg Roach                    ->on('individual_placelinks.pl_file', '=', 'individuals.i_file')
488*dfb2cda2SGreg Roach                    ->on('individual_placelinks.pl_gid', '=', 'individuals.i_id');
489*dfb2cda2SGreg Roach            });
490*dfb2cda2SGreg Roach            $query->join('places AS individual_places', function (JoinClause $join): void {
491*dfb2cda2SGreg Roach                $join
492*dfb2cda2SGreg Roach                    ->on('individual_places.p_file', '=', 'individual_placelinks.pl_file')
493*dfb2cda2SGreg Roach                    ->on('individual_places.p_id', '=', 'individual_placelinks.pl_p_id');
494*dfb2cda2SGreg Roach            });
495*dfb2cda2SGreg Roach        }
496*dfb2cda2SGreg Roach
497*dfb2cda2SGreg Roach        if ($fam_plac) {
498*dfb2cda2SGreg Roach            $query->join('placelinks AS familyl_placelinks', function (JoinClause $join): void {
499*dfb2cda2SGreg Roach                $join
500*dfb2cda2SGreg Roach                    ->on('familyl_placelinks.pl_file', '=', 'individuals.i_file')
501*dfb2cda2SGreg Roach                    ->on('familyl_placelinks.pl_gid', '=', 'individuals.i_id');
502*dfb2cda2SGreg Roach            });
503*dfb2cda2SGreg Roach            $query->join('places AS family_places', function (JoinClause $join): void {
504*dfb2cda2SGreg Roach                $join
505*dfb2cda2SGreg Roach                    ->on('family_places.p_file', '=', 'familyl_placelinks.pl_file')
506*dfb2cda2SGreg Roach                    ->on('family_places.p_id', '=', 'familyl_placelinks.pl_p_id');
507*dfb2cda2SGreg Roach            });
508*dfb2cda2SGreg Roach        }
509*dfb2cda2SGreg Roach
510*dfb2cda2SGreg Roach        foreach ($fields as $field_name => $field_value) {
511*dfb2cda2SGreg Roach            $parts = preg_split('/:/', $field_name . '::::');
512*dfb2cda2SGreg Roach            if ($parts[0] === 'NAME') {
513*dfb2cda2SGreg Roach                // NAME:*
514*dfb2cda2SGreg Roach                switch ($parts[1]) {
515*dfb2cda2SGreg Roach                    case 'GIVN':
516*dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
517*dfb2cda2SGreg Roach                            case 'EXACT':
518*dfb2cda2SGreg Roach                                $query->where('individual_name.n_givn', '=', $field_value);
519*dfb2cda2SGreg Roach                                break;
520*dfb2cda2SGreg Roach                            case 'BEGINS':
521*dfb2cda2SGreg Roach                                $query->where('individual_name.n_givn', 'LIKE', $field_value . '%');
522*dfb2cda2SGreg Roach                                break;
523*dfb2cda2SGreg Roach                            case 'CONTAINS':
524*dfb2cda2SGreg Roach                                $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
525*dfb2cda2SGreg Roach                                break;
526*dfb2cda2SGreg Roach                            case 'SDX_STD':
527*dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
528*dfb2cda2SGreg Roach                                if ($sdx !== '') {
529*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_givn_std', $sdx);
530*dfb2cda2SGreg Roach                                } else {
531*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
532*dfb2cda2SGreg Roach                                    $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
533*dfb2cda2SGreg Roach                                }
534*dfb2cda2SGreg Roach                                break;
535*dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
536*dfb2cda2SGreg Roach                            case 'SDX_DM':
537*dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
538*dfb2cda2SGreg Roach                                if ($sdx !== '') {
539*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_givn_dm', $sdx);
540*dfb2cda2SGreg Roach                                } else {
541*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
542*dfb2cda2SGreg Roach                                    $query->where('individual_name.n_givn', 'LIKE', '%' . $field_value . '%');
543*dfb2cda2SGreg Roach                                }
544*dfb2cda2SGreg Roach                                break;
545*dfb2cda2SGreg Roach                        }
546*dfb2cda2SGreg Roach                        break;
547*dfb2cda2SGreg Roach                    case 'SURN':
548*dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
549*dfb2cda2SGreg Roach                            case 'EXACT':
550*dfb2cda2SGreg Roach                                $query->where('individual_name.n_surn', '=', $field_value);
551*dfb2cda2SGreg Roach                                break;
552*dfb2cda2SGreg Roach                            case 'BEGINS':
553*dfb2cda2SGreg Roach                                $query->where('individual_name.n_surn', 'LIKE', $field_value . '%');
554*dfb2cda2SGreg Roach                                break;
555*dfb2cda2SGreg Roach                            case 'CONTAINS':
556*dfb2cda2SGreg Roach                                $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%');
557*dfb2cda2SGreg Roach                                break;
558*dfb2cda2SGreg Roach                            case 'SDX_STD':
559*dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
560*dfb2cda2SGreg Roach                                if ($sdx !== '') {
561*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_surn_std', $sdx);
562*dfb2cda2SGreg Roach                                } else {
563*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
564*dfb2cda2SGreg Roach                                    $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%');
565*dfb2cda2SGreg Roach                                }
566*dfb2cda2SGreg Roach                                break;
567*dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
568*dfb2cda2SGreg Roach                            case 'SDX_DM':
569*dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
570*dfb2cda2SGreg Roach                                if ($sdx !== '') {
571*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, 'individual_name.n_soundex_surn_dm', $sdx);
572*dfb2cda2SGreg Roach                                } else {
573*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
574*dfb2cda2SGreg Roach                                    $query->where('individual_name.n_surn', 'LIKE', '%' . $field_value . '%');
575*dfb2cda2SGreg Roach                                }
576*dfb2cda2SGreg Roach                                break;
577*dfb2cda2SGreg Roach                        }
578*dfb2cda2SGreg Roach                        break;
579*dfb2cda2SGreg Roach                    case 'NICK':
580*dfb2cda2SGreg Roach                    case '_MARNM':
581*dfb2cda2SGreg Roach                    case '_HEB':
582*dfb2cda2SGreg Roach                    case '_AKA':
583*dfb2cda2SGreg Roach                        $query
584*dfb2cda2SGreg Roach                            ->where('individual_name', '=', $parts[1])
585*dfb2cda2SGreg Roach                            ->where('individual_name', 'LIKE', '%' . $field_value . '%');
586*dfb2cda2SGreg Roach                        break;
587*dfb2cda2SGreg Roach                }
588*dfb2cda2SGreg Roach                unset($fields[$field_name]);
589*dfb2cda2SGreg Roach            } elseif ($parts[1] === 'DATE') {
590*dfb2cda2SGreg Roach                // *:DATE
591*dfb2cda2SGreg Roach                $date = new Date($field_value);
592*dfb2cda2SGreg Roach                if ($date->isOK()) {
593*dfb2cda2SGreg Roach                    $delta = 365 * ($modifiers[$field_name] ?? 0);
594*dfb2cda2SGreg Roach                    $query
595*dfb2cda2SGreg Roach                        ->where('individual_dates.d_fact', '=', $parts[0])
596*dfb2cda2SGreg Roach                        ->where('individual_dates.d_julianday1', '>=', $date->minimumJulianDay() - $delta)
597*dfb2cda2SGreg Roach                        ->where('individual_dates.d_julianday2', '<=', $date->minimumJulianDay() + $delta);
598*dfb2cda2SGreg Roach                }
599*dfb2cda2SGreg Roach                unset($fields[$field_name]);
600*dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMS' && $parts[2] === 'DATE') {
601*dfb2cda2SGreg Roach                // FAMS:*:DATE
602*dfb2cda2SGreg Roach                $date = new Date($field_value);
603*dfb2cda2SGreg Roach                if ($date->isOK()) {
604*dfb2cda2SGreg Roach                    $delta = 365 * $modifiers[$field_name];
605*dfb2cda2SGreg Roach                    $query
606*dfb2cda2SGreg Roach                        ->where('family_dates.d_fact', '=', $parts[1])
607*dfb2cda2SGreg Roach                        ->where('family_dates.d_julianday1', '>=', $date->minimumJulianDay() - $delta)
608*dfb2cda2SGreg Roach                        ->where('family_dates.d_julianday2', '<=', $date->minimumJulianDay() + $delta);
609*dfb2cda2SGreg Roach                }
610*dfb2cda2SGreg Roach                unset($fields[$field_name]);
611*dfb2cda2SGreg Roach            } elseif ($parts[1] === 'PLAC') {
612*dfb2cda2SGreg Roach                // *:PLAC
613*dfb2cda2SGreg Roach                // SQL can only link a place to a person/family, not to an event.
614*dfb2cda2SGreg Roach                $query->where('individual_places.p_place', 'LIKE', '%' . $field_value . '%');
615*dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') {
616*dfb2cda2SGreg Roach                // FAMS:*:PLAC
617*dfb2cda2SGreg Roach                // SQL can only link a place to a person/family, not to an event.
618*dfb2cda2SGreg Roach                $query->where('family_places.p_place', 'LIKE', '%' . $field_value . '%');
619*dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMC' && $parts[2] === 'NAME') {
620*dfb2cda2SGreg Roach                $table = $parts[1] === 'HUSB' ? 'father_name' : 'mother_name';
621*dfb2cda2SGreg Roach                // NAME:*
622*dfb2cda2SGreg Roach                switch ($parts[3]) {
623*dfb2cda2SGreg Roach                    case 'GIVN':
624*dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
625*dfb2cda2SGreg Roach                            case 'EXACT':
626*dfb2cda2SGreg Roach                                $query->where($table . '.n_givn', '=', $field_value);
627*dfb2cda2SGreg Roach                                break;
628*dfb2cda2SGreg Roach                            case 'BEGINS':
629*dfb2cda2SGreg Roach                                $query->where($table . '.n_givn', 'LIKE', $field_value . '%');
630*dfb2cda2SGreg Roach                                break;
631*dfb2cda2SGreg Roach                            case 'CONTAINS':
632*dfb2cda2SGreg Roach                                $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
633*dfb2cda2SGreg Roach                                break;
634*dfb2cda2SGreg Roach                            case 'SDX_STD':
635*dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
636*dfb2cda2SGreg Roach                                if ($sdx !== '') {
637*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_givn_std', $sdx);
638*dfb2cda2SGreg Roach                                } else {
639*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
640*dfb2cda2SGreg Roach                                    $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
641*dfb2cda2SGreg Roach                                }
642*dfb2cda2SGreg Roach                                break;
643*dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
644*dfb2cda2SGreg Roach                            case 'SDX_DM':
645*dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
646*dfb2cda2SGreg Roach                                if ($sdx !== '') {
647*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_givn_dm', $sdx);
648*dfb2cda2SGreg Roach                                } else {
649*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
650*dfb2cda2SGreg Roach                                    $query->where($table . '.n_givn', 'LIKE', '%' . $field_value . '%');
651*dfb2cda2SGreg Roach                                }
652*dfb2cda2SGreg Roach                                break;
653*dfb2cda2SGreg Roach                        }
654*dfb2cda2SGreg Roach                        break;
655*dfb2cda2SGreg Roach                    case 'SURN':
656*dfb2cda2SGreg Roach                        switch ($modifiers[$field_name]) {
657*dfb2cda2SGreg Roach                            case 'EXACT':
658*dfb2cda2SGreg Roach                                $query->where($table . '.n_surn', '=', $field_value);
659*dfb2cda2SGreg Roach                                break;
660*dfb2cda2SGreg Roach                            case 'BEGINS':
661*dfb2cda2SGreg Roach                                $query->where($table . '.n_surn', 'LIKE', $field_value . '%');
662*dfb2cda2SGreg Roach                                break;
663*dfb2cda2SGreg Roach                            case 'CONTAINS':
664*dfb2cda2SGreg Roach                                $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
665*dfb2cda2SGreg Roach                                break;
666*dfb2cda2SGreg Roach                            case 'SDX_STD':
667*dfb2cda2SGreg Roach                                $sdx = Soundex::russell($field_value);
668*dfb2cda2SGreg Roach                                if ($sdx !== '') {
669*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_surn_std', $sdx);
670*dfb2cda2SGreg Roach                                } else {
671*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
672*dfb2cda2SGreg Roach                                    $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
673*dfb2cda2SGreg Roach                                }
674*dfb2cda2SGreg Roach                                break;
675*dfb2cda2SGreg Roach                            case 'SDX': // SDX uses DM by default.
676*dfb2cda2SGreg Roach                            case 'SDX_DM':
677*dfb2cda2SGreg Roach                                $sdx = Soundex::daitchMokotoff($field_value);
678*dfb2cda2SGreg Roach                                if ($sdx !== '') {
679*dfb2cda2SGreg Roach                                    $this->wherePhonetic($query, $table . '.n_soundex_surn_dm', $sdx);
680*dfb2cda2SGreg Roach                                } else {
681*dfb2cda2SGreg Roach                                    // No phonetic content? Use a substring match
682*dfb2cda2SGreg Roach                                    $query->where($table . '.n_surn', 'LIKE', '%' . $field_value . '%');
683*dfb2cda2SGreg Roach                                }
684*dfb2cda2SGreg Roach                                break;
685*dfb2cda2SGreg Roach                        }
686*dfb2cda2SGreg Roach                        break;
687*dfb2cda2SGreg Roach                }
688*dfb2cda2SGreg Roach                unset($fields[$field_name]);
689*dfb2cda2SGreg Roach            } elseif ($parts[0] === 'FAMS') {
690*dfb2cda2SGreg Roach                // e.g. searches for occupation, religion, note, etc.
691*dfb2cda2SGreg Roach                // Initial matching only.  Need PHP to apply filter.
692*dfb2cda2SGreg Roach                $query->where('families.f_gedcom', 'LIKE', "%\n1 " . $parts[1] . ' %' . $field_value . '%');
693*dfb2cda2SGreg Roach            } elseif ($parts[1] === 'TYPE') {
694*dfb2cda2SGreg Roach                // e.g. FACT:TYPE or EVEN:TYPE
695*dfb2cda2SGreg Roach                // Initial matching only.  Need PHP to apply filter.
696*dfb2cda2SGreg Roach                $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . '%\n2 TYPE %' . $field_value . '%');
697*dfb2cda2SGreg Roach            } else {
698*dfb2cda2SGreg Roach                // e.g. searches for occupation, religion, note, etc.
699*dfb2cda2SGreg Roach                // Initial matching only.  Need PHP to apply filter.
700*dfb2cda2SGreg Roach                $query->where('individuals.i_gedcom', 'LIKE', "%\n1 " . $parts[0] . ' %' . $field_value . '%');
701*dfb2cda2SGreg Roach            }
702*dfb2cda2SGreg Roach        }
703*dfb2cda2SGreg Roach
704*dfb2cda2SGreg Roach        return $query
705*dfb2cda2SGreg Roach            ->get()
706*dfb2cda2SGreg Roach            ->map(Individual::rowMapper())
707*dfb2cda2SGreg Roach            ->filter(GedcomRecord::accessFilter())
708*dfb2cda2SGreg Roach            ->filter(function (Individual $individual) use ($fields): bool {
709*dfb2cda2SGreg Roach                if (empty($fields)) {
710*dfb2cda2SGreg Roach                    return true;
711*dfb2cda2SGreg Roach                }
712*dfb2cda2SGreg Roach
713*dfb2cda2SGreg Roach                // Check for XXXX:PLAC fields, which were only partially matched by SQL
714*dfb2cda2SGreg Roach                foreach ($fields as $field_name => $field_value) {
715*dfb2cda2SGreg Roach                    $regex_field_value = preg_quote($field_value, '/');
716*dfb2cda2SGreg Roach
717*dfb2cda2SGreg Roach                    $parts = preg_split('/:/', $field_name . '::::');
718*dfb2cda2SGreg Roach
719*dfb2cda2SGreg Roach                    if ($parts[1] === 'PLAC') {
720*dfb2cda2SGreg Roach                        // *:PLAC
721*dfb2cda2SGreg Roach                        if (preg_match('/\n\d PLAC .*' . $regex_field_value . '/i', $individual->gedcom())) {
722*dfb2cda2SGreg Roach                            continue;
723*dfb2cda2SGreg Roach                        }
724*dfb2cda2SGreg Roach                    } elseif ($parts[0] === 'FAMS' && $parts[2] === 'PLAC') {
725*dfb2cda2SGreg Roach                        // FAMS:*:PLAC
726*dfb2cda2SGreg Roach                        foreach ($individual->getSpouseFamilies() as $family) {
727*dfb2cda2SGreg Roach                            if (preg_match('/\n\d PLAC .*' . $regex_field_value . '/i', $family->gedcom())) {
728*dfb2cda2SGreg Roach                                continue;
729*dfb2cda2SGreg Roach                            }
730*dfb2cda2SGreg Roach                        }
731*dfb2cda2SGreg Roach                    } elseif ($parts[0] === 'FAMS') {
732*dfb2cda2SGreg Roach                        // e.g. searches for occupation, religion, note, etc.
733*dfb2cda2SGreg Roach                        foreach ($individual->getSpouseFamilies() as $family) {
734*dfb2cda2SGreg Roach                            if (preg_match('/\n1 ' . $parts[1] . ' .*' . $regex_field_value . '/i', $family->gedcom())) {
735*dfb2cda2SGreg Roach                                continue;
736*dfb2cda2SGreg Roach                            }
737*dfb2cda2SGreg Roach                        }
738*dfb2cda2SGreg Roach                    } elseif ($parts[1] === 'TYPE') {
739*dfb2cda2SGreg Roach                        // e.g. FACT:TYPE or EVEN:TYPE
740*dfb2cda2SGreg Roach                        if (preg_match('/\n1 ' . $parts[0] . '.*(\n2.*)*2 TYPE .*' . $regex_field_value . '/i', $individual->gedcom())) {
741*dfb2cda2SGreg Roach                            continue;
742*dfb2cda2SGreg Roach                        }
743*dfb2cda2SGreg Roach                    } else {
744*dfb2cda2SGreg Roach                        // e.g. searches for occupation, religion, note, etc.
745*dfb2cda2SGreg Roach                        if (preg_match('/\n1 ' . $parts[0] . ' .*' . $regex_field_value . '/i', $individual->gedcom())) {
746*dfb2cda2SGreg Roach                            continue;
747*dfb2cda2SGreg Roach                        }
748*dfb2cda2SGreg Roach                    }
749*dfb2cda2SGreg Roach
750*dfb2cda2SGreg Roach                    return false;
751*dfb2cda2SGreg Roach                }
752*dfb2cda2SGreg Roach
753*dfb2cda2SGreg Roach                return true;
754*dfb2cda2SGreg Roach            });
755*dfb2cda2SGreg Roach    }
756*dfb2cda2SGreg Roach
757*dfb2cda2SGreg Roach    /**
758*dfb2cda2SGreg Roach     * @param string $soundex
759*dfb2cda2SGreg Roach     * @param string $lastname
760*dfb2cda2SGreg Roach     * @param string $firstname
761*dfb2cda2SGreg Roach     * @param string $place
762*dfb2cda2SGreg Roach     * @param Tree[] $search_trees
763*dfb2cda2SGreg Roach     *
764*dfb2cda2SGreg Roach     * @return Collection|Individual[]
765*dfb2cda2SGreg Roach     */
766*dfb2cda2SGreg Roach    public function searchIndividualsPhonetic(string $soundex, string $lastname, string $firstname, string $place, array $search_trees): Collection
767*dfb2cda2SGreg Roach    {
768*dfb2cda2SGreg Roach        switch ($soundex) {
769*dfb2cda2SGreg Roach            default:
770*dfb2cda2SGreg Roach            case 'Russell':
771*dfb2cda2SGreg Roach                $givn_sdx   = Soundex::russell($firstname);
772*dfb2cda2SGreg Roach                $surn_sdx   = Soundex::russell($lastname);
773*dfb2cda2SGreg Roach                $plac_sdx   = Soundex::russell($place);
774*dfb2cda2SGreg Roach                $givn_field = 'n_soundex_givn_std';
775*dfb2cda2SGreg Roach                $surn_field = 'n_soundex_surn_std';
776*dfb2cda2SGreg Roach                $plac_field = 'p_std_soundex';
777*dfb2cda2SGreg Roach                break;
778*dfb2cda2SGreg Roach            case 'DaitchM':
779*dfb2cda2SGreg Roach                $givn_sdx   = Soundex::daitchMokotoff($firstname);
780*dfb2cda2SGreg Roach                $surn_sdx   = Soundex::daitchMokotoff($lastname);
781*dfb2cda2SGreg Roach                $plac_sdx   = Soundex::daitchMokotoff($place);
782*dfb2cda2SGreg Roach                $givn_field = 'n_soundex_givn_dm';
783*dfb2cda2SGreg Roach                $surn_field = 'n_soundex_surn_dm';
784*dfb2cda2SGreg Roach                $plac_field = 'p_dm_soundex';
785*dfb2cda2SGreg Roach                break;
786*dfb2cda2SGreg Roach        }
787*dfb2cda2SGreg Roach
788*dfb2cda2SGreg Roach        // Nothing to search for? Return nothing.
789*dfb2cda2SGreg Roach        if ($givn_sdx === '' && $surn_sdx === '' && $plac_sdx === '') {
790*dfb2cda2SGreg Roach            return new Collection;
791*dfb2cda2SGreg Roach        }
792*dfb2cda2SGreg Roach
793*dfb2cda2SGreg Roach        $query = DB::table('individuals')
794*dfb2cda2SGreg Roach            ->select(['individuals.*'])
795*dfb2cda2SGreg Roach            ->distinct();
796*dfb2cda2SGreg Roach
797*dfb2cda2SGreg Roach        $this->whereTrees($query, 'i_file', $search_trees);
798*dfb2cda2SGreg Roach
799*dfb2cda2SGreg Roach        if ($plac_sdx !== '') {
800*dfb2cda2SGreg Roach            $query->join('placelinks', function (JoinClause $join): void {
801*dfb2cda2SGreg Roach                $join
802*dfb2cda2SGreg Roach                    ->on('placelinks.pl_file', '=', 'individuals.i_file')
803*dfb2cda2SGreg Roach                    ->on('placelinks.pl_gid', '=', 'individuals.i_id');
804*dfb2cda2SGreg Roach            });
805*dfb2cda2SGreg Roach            $query->join('places', function (JoinClause $join): void {
806*dfb2cda2SGreg Roach                $join
807*dfb2cda2SGreg Roach                    ->on('places.p_file', '=', 'placelinks.pl_file')
808*dfb2cda2SGreg Roach                    ->on('places.p_id', '=', 'placelinks.pl_p_id');
809*dfb2cda2SGreg Roach            });
810*dfb2cda2SGreg Roach
811*dfb2cda2SGreg Roach            $this->wherePhonetic($query, $plac_field, $plac_sdx);
812*dfb2cda2SGreg Roach        }
813*dfb2cda2SGreg Roach
814*dfb2cda2SGreg Roach        if ($givn_sdx !== '' || $surn_sdx !== '') {
815*dfb2cda2SGreg Roach            $query->join('name', function (JoinClause $join): void {
816*dfb2cda2SGreg Roach                $join
817*dfb2cda2SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
818*dfb2cda2SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id');
819*dfb2cda2SGreg Roach            });
820*dfb2cda2SGreg Roach
821*dfb2cda2SGreg Roach            $this->wherePhonetic($query, $givn_field, $givn_sdx);
822*dfb2cda2SGreg Roach            $this->wherePhonetic($query, $surn_field, $surn_sdx);
823*dfb2cda2SGreg Roach        }
824*dfb2cda2SGreg Roach
825*dfb2cda2SGreg Roach        return $query
826*dfb2cda2SGreg Roach            ->get()
827*dfb2cda2SGreg Roach            ->map(Individual::rowMapper())
828*dfb2cda2SGreg Roach            ->filter(GedcomRecord::accessFilter());
829*dfb2cda2SGreg Roach    }
830*dfb2cda2SGreg Roach
831*dfb2cda2SGreg Roach    /**
83232cd2800SGreg Roach     * Paginate a search query.
83332cd2800SGreg Roach     *
83432cd2800SGreg Roach     * @param Builder $query      Searches the database for the desired records.
83532cd2800SGreg Roach     * @param Closure $row_mapper Converts a row from the query into a record.
836a7a24840SGreg Roach     * @param Closure $row_filter
83732cd2800SGreg Roach     * @param int     $offset     Skip this many rows.
83832cd2800SGreg Roach     * @param int     $limit      Take this many rows.
83932cd2800SGreg Roach     *
84032cd2800SGreg Roach     * @return Collection
84132cd2800SGreg Roach     */
842a7a24840SGreg Roach    private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection
84332cd2800SGreg Roach    {
84432cd2800SGreg Roach        $collection = new Collection();
84532cd2800SGreg Roach
84632cd2800SGreg Roach        foreach ($query->cursor() as $row) {
84732cd2800SGreg Roach            $record = $row_mapper($row);
848b68caec6SGreg Roach            // If the object has a method "canShow()", then use it to filter for privacy.
849a7a24840SGreg Roach            if ($row_filter($record)) {
85032cd2800SGreg Roach                if ($offset > 0) {
85132cd2800SGreg Roach                    $offset--;
85232cd2800SGreg Roach                } else {
85332cd2800SGreg Roach                    if ($limit > 0) {
85432cd2800SGreg Roach                        $collection->push($record);
85532cd2800SGreg Roach                    }
85632cd2800SGreg Roach
85732cd2800SGreg Roach                    $limit--;
85832cd2800SGreg Roach
85932cd2800SGreg Roach                    if ($limit === 0) {
86032cd2800SGreg Roach                        break;
86132cd2800SGreg Roach                    }
86232cd2800SGreg Roach                }
86332cd2800SGreg Roach            }
86432cd2800SGreg Roach        }
86532cd2800SGreg Roach
86632cd2800SGreg Roach        return $collection;
86732cd2800SGreg Roach    }
868a7a24840SGreg Roach
869a7a24840SGreg Roach    /**
870a7a24840SGreg Roach     * Apply search filters to a SQL query column.  Apply collation rules to MySQL.
871a7a24840SGreg Roach     *
872a7a24840SGreg Roach     * @param Builder           $query
873a7a24840SGreg Roach     * @param Expression|string $field
874a7a24840SGreg Roach     * @param string[]          $search_terms
875a7a24840SGreg Roach     */
876a7a24840SGreg Roach    private function whereSearch(Builder $query, $field, array $search_terms): void
877a7a24840SGreg Roach    {
878a7a24840SGreg Roach        if ($field instanceof Expression) {
879a7a24840SGreg Roach            $field = $field->getValue();
880a7a24840SGreg Roach        }
881a7a24840SGreg Roach
882a7a24840SGreg Roach        $field = DB::raw($field . ' /*! COLLATE ' . 'utf8_' . $this->locale->collation() . ' */');
883a7a24840SGreg Roach
884a7a24840SGreg Roach        foreach ($search_terms as $search_term) {
885a7a24840SGreg Roach            $query->whereContains($field, $search_term);
886a7a24840SGreg Roach        }
887a7a24840SGreg Roach    }
888a7a24840SGreg Roach
889a7a24840SGreg Roach    /**
8902d686e68SGreg Roach     * Apply soundex search filters to a SQL query column.
8912d686e68SGreg Roach     *
8922d686e68SGreg Roach     * @param Builder           $query
8932d686e68SGreg Roach     * @param Expression|string $field
8942d686e68SGreg Roach     * @param string            $soundex
8952d686e68SGreg Roach     */
8962d686e68SGreg Roach    private function wherePhonetic(Builder $query, $field, string $soundex): void
8972d686e68SGreg Roach    {
8982d686e68SGreg Roach        if ($soundex !== '') {
8992d686e68SGreg Roach            $query->where(function (Builder $query) use ($soundex, $field): void {
9002d686e68SGreg Roach                foreach (explode(':', $soundex) as $sdx) {
9012d686e68SGreg Roach                    $query->orWhere($field, 'LIKE', '%' . $sdx . '%');
9022d686e68SGreg Roach                }
9032d686e68SGreg Roach            });
9042d686e68SGreg Roach        }
9052d686e68SGreg Roach    }
9062d686e68SGreg Roach
9072d686e68SGreg Roach    /**
908a7a24840SGreg Roach     * @param Builder $query
909a7a24840SGreg Roach     * @param string  $tree_id_field
910a7a24840SGreg Roach     * @param Tree[]  $trees
911a7a24840SGreg Roach     */
912a7a24840SGreg Roach    private function whereTrees(Builder $query, string $tree_id_field, array $trees): void
913a7a24840SGreg Roach    {
914a7a24840SGreg Roach        $tree_ids = array_map(function (Tree $tree) {
915a7a24840SGreg Roach            return $tree->id();
916a7a24840SGreg Roach        }, $trees);
917a7a24840SGreg Roach
918a7a24840SGreg Roach        $query->whereIn($tree_id_field, $tree_ids);
919a7a24840SGreg Roach    }
920a7a24840SGreg Roach
921a7a24840SGreg Roach    /**
922a7a24840SGreg Roach     * A closure to filter records by privacy-filtered GEDCOM data.
923a7a24840SGreg Roach     *
924a7a24840SGreg Roach     * @param array $search_terms
925a7a24840SGreg Roach     *
926a7a24840SGreg Roach     * @return Closure
927a7a24840SGreg Roach     */
928a7a24840SGreg Roach    private function rawGedcomFilter(array $search_terms): Closure
929a7a24840SGreg Roach    {
930a7a24840SGreg Roach        return function (GedcomRecord $record) use ($search_terms): bool {
931a7a24840SGreg Roach            // Ignore non-genealogy fields
932a7a24840SGreg Roach            $gedcom = preg_replace('/\n\d (?:_UID) .*/', '', $record->gedcom());
933a7a24840SGreg Roach
934a7a24840SGreg Roach            // Ignore matches in links
935a7a24840SGreg Roach            $gedcom = preg_replace('/\n\d ' . Gedcom::REGEX_TAG . '( @' . Gedcom::REGEX_XREF . '@)?/', '', $gedcom);
936a7a24840SGreg Roach
937a7a24840SGreg Roach            // Re-apply the filtering
938a7a24840SGreg Roach            foreach ($search_terms as $search_term) {
939a7a24840SGreg Roach                if (mb_stripos($gedcom, $search_term) === false) {
940a7a24840SGreg Roach                    return false;
941a7a24840SGreg Roach                }
942a7a24840SGreg Roach            }
943a7a24840SGreg Roach
944a7a24840SGreg Roach            return true;
945a7a24840SGreg Roach        };
946a7a24840SGreg Roach    }
94732cd2800SGreg Roach}
948