Extract three functions from the ~140-line .build_suggestions() orchestrator to comply with the 'functions under 50 lines' convention: - .query_candidate_recipes(): candidate recipe lookup by category/subtype scope, plus M/L self-exclusion - .query_recipe_meta(): metadata lookup for labels and year spans - .query_covered_years(): Path 1 gap-year coverage query via the recipe's own generic join; returns empty data frame when gap_years is empty Kept inline per design: the for-loop that merges covered-years + suppressed-components into suggestion objects, the M/L-exclusion comment block as call-site rationale, and .attach_ig_counterparts() at the end. Pure extraction, no behavior change -- SQL text is unchanged apart from whitespace. Restored real multi-line SQL string literals in the two new helpers (the original candidate/covered-years queries were written that way; keep it consistent with .query_recipe_meta()) and normal roxygen '#'' comment-marker spacing throughout, both of which drifted during extraction in an earlier pass. All 1084 tests pass (2 skipped live-corpus), measured devtools::test() against this commit in a clean worktree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>