The cursor is the created_utc of the last row in the page you got back. Pass it forward as after when sorting asc, or as before when sorting desc (the default), to fetch the next page. has_moreis true whenever the page came back full — keep going until it's false or you have enough. This applies to /v1/posts and /v1/comments in listing mode — their ids= batch mode and every other endpoint always return cursor: null, has_more: false.
q= is a client-side exact-phrase filter (with basic singular/plural flexibility) applied to whatever page comes back — it is not server-side search, and it only ever looks at the single page this call fetched (up to limit, max 100), never the whole after/before window in one shot. You pay 1 credit per page fetched whether it matched your phrase or not, and cursor/has_more always describe the raw page, not the filtered result — a page that comes back with 0 rows after filtering does notmean there's nothing left; it means keep paginating with the returned cursor.
Concretely: a single call with q= set against a moderately busy subreddit can cover as little as a few minutes of history, not the whole date range you asked for — for comments especially, one page is often just the last few minutes on an active subreddit. Scanning several days for one exact phrase can genuinely take dozens of calls. Scope your window with after/before to control how much you scan (and pay for) per search, and always check has_more before concluding a search came back empty.