# Finding a person or company

`listing` answers a bounded, searched, paged set of records from the database rather than from a slice held in memory. The count is one aggregate, the filter and the page are SQL, and a `%` or `_` typed into the box is escaped and matched as a character. Results are bounded on purpose — a page you can read beats a list nobody scrolls.

- **Status:** Available
- **Audience:** both
- **In the app:** #/relationships, #/companies, #/data
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/relationships/relationship-search/

## What a search actually reads

The searchable material is what identifies a record to a colleague: the display name, the company it belongs to, and the addresses that resolve to it. Looking somebody up by an address they wrote from is the common case, and it is the same resolution `by_handle` performs when a message arrives — one indexed lookup rather than a scan.

Message bodies are not what this box searches. Finding a conversation by something said in it is the Conversations screen's job, and the Assistant's `find_anything` is the tool for a question that crosses records.

## The defect this design came from

The conversation list once read 400 rows and did the counting, filtering, searching and paging in Python over that slice. On a workspace with 430 conversations whose 25 closed ones were the oldest, the results were not merely slow — they were wrong:

| What you did | What you got | Why |
|---|---|---|
| Opened the Closed tab | Nothing at all | The 25 closed rows were the oldest, and the 400-row slice never reached them |
| Read the total | 400 | The total counted the slice rather than the workspace |
| Asked for page five | An empty page | Paging ran over the slice, which had already ended |

Counts are one `GROUP BY` now; the filter and the page are SQL. The lesson generalises past this one screen: a limit applied before the question is answered produces a confident wrong answer, and confident wrong answers are harder to notice than slow ones.

## Wildcards you did not mean to type

`%` and `_` are wildcards in a SQL `LIKE`. Somebody searching for an account code containing an underscore, or a discount note containing a per cent sign, was matching far more than they asked for — an underscore quietly meant *any single character*.

```text
typed:    ACME_01
matched:  ACME_01, ACME-01, ACME 01, ACMEX01  (before)
matched:  ACME_01                                (now)
```

Both characters are escaped before the query runs and matched literally. This is a small correctness fix with a large trust consequence: a search that returns extra rows teaches people not to believe the result, and a search nobody believes gets replaced by scrolling.

## Why results are bounded

- **A page is a page.** `listing` returns a window with a total, so the count is honest even though the rows on screen are not all of them.
- **Duplicate proposals are capped** at a readable number — a queue of pairs is for reading, not for bulk acceptance.
- **A person's history is bounded** at 200 entries by default, newest first, which is where the interesting part of a relationship is.
- **Cost must not track the page size.** The measured pattern behind that is on [the timeline](/docs/relationships/timeline/): per-row follow-up queries turned 40 rows into 133 statements, and the set-based rewrite made it 15, flat.

> **Note** If you are searching because you suspect a second record for the same human, the Data grid's `people` sheet is the better instrument — sort, group and read the pairs — and [duplicate detection](/docs/relationships/duplicates/) is what proposes the merge.

## Questions

### Why does a search for an underscore no longer return extra rows?

Because `%` and `_` are escaped before the query runs. They used to reach the database as wildcards, so an account code with an underscore matched anything with any character in that position.

### Can I search by phone number?

Yes — the addresses that resolve to a record are searchable, which includes phone identities. The same normalisation used for resolution applies, so spacing does not decide whether you find somebody.

### Why is the total sometimes larger than the rows I can see?

Because the total is a real count of what matches and the rows are one page of it. That is the fix for a screen which used to count only the slice it had already loaded, and then disagreed with itself.

## Related

- [Relationships in Connect](https://connectbyjbrh.com/docs/relationships/)
- [Duplicate detection](https://connectbyjbrh.com/docs/relationships/duplicates/)
- [The customer timeline](https://connectbyjbrh.com/docs/relationships/timeline/)
- [Files and data in Connect](https://connectbyjbrh.com/docs/files-data/)
- [Connect Assistant](https://connectbyjbrh.com/docs/assistant/)
- [Headline numbers that stop at the page size](https://connectbyjbrh.com/research/counting-past-the-limit/)

## What this page is based on

- `docs-source/sources/CHANNELS.md` §5 — the conversation-list defect and its fix
- `docs-source/sources/GENERAL.md` §11 — measured query cost
- `docs-source/sources/CHANNELS.md` §5 — `listing`, `by_handle` and `duplicates`
- Connect capability registry (docs-source/facts.py)
